feat(header): 新增业务线切换器组件
- 新增 stores/businessLine.ts:Pinia 状态管理,持久化到 localStorage - 新增 components/BusinessLineSwitcher.vue:企业微信式下拉切换器 - 修改 AppHeader.vue:在环境标签后集成切换器 - 预置 5 条业务线 mock 数据(LAS/Kodo/LTOKEN/灵矽/MAAS) - 更新 components.d.ts 自动类型声明
This commit is contained in:
Vendored
+1
@@ -11,6 +11,7 @@ declare module 'vue' {
|
||||
AppMain: typeof import('./src/components/Layout/AppMain.vue')['default']
|
||||
AppSidebar: typeof import('./src/components/Layout/AppSidebar.vue')['default']
|
||||
AuditLogTable: typeof import('./src/components/AuditLogTable.vue')['default']
|
||||
BusinessLineSwitcher: typeof import('./src/components/BusinessLineSwitcher.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div class="bl-switch" ref="switchRef">
|
||||
<div class="bl-trigger" @click="toggleMenu">
|
||||
<div
|
||||
class="bl-ic"
|
||||
:style="{ background: currentBL?.iconBg, color: currentBL?.iconColor }"
|
||||
>
|
||||
{{ currentBL?.iconText }}
|
||||
</div>
|
||||
<span class="bl-name-trigger">{{ currentBL?.name || '未选择' }}</span>
|
||||
<span class="bl-role" v-if="currentBL">{{ currentBL.role }} · 授权 {{ authCount }} 子系统</span>
|
||||
<span class="caret">▼</span>
|
||||
</div>
|
||||
<Transition name="bl-menu">
|
||||
<div class="bl-menu" v-show="menuVisible">
|
||||
<div class="bl-menu-label">切换业务线(仅展示被授权范围)</div>
|
||||
<div
|
||||
v-for="bl in businessLines"
|
||||
:key="bl.id"
|
||||
class="bl-opt"
|
||||
:class="{ sel: bl.id === currentBL?.id, locked: !bl.authorized }"
|
||||
@click="handleSelect(bl)"
|
||||
>
|
||||
<div
|
||||
class="bl-ic"
|
||||
:style="{ background: bl.iconBg, color: bl.iconColor }"
|
||||
>
|
||||
{{ bl.iconText }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="bl-name">{{ bl.name }}</div>
|
||||
<div class="bl-sub">{{ bl.ou }} · {{ bl.role }}</div>
|
||||
</div>
|
||||
<span v-if="bl.authorized" class="bl-check">✓</span>
|
||||
<span v-else class="bl-lock">🔒 无权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useBusinessLineStore, type BusinessLine } from '@/stores/businessLine'
|
||||
|
||||
const blStore = useBusinessLineStore()
|
||||
|
||||
const businessLines = computed(() => blStore.businessLines)
|
||||
const currentBL = computed(() => blStore.currentBL)
|
||||
const authCount = computed(() => blStore.authorizedBLs.length)
|
||||
|
||||
const menuVisible = ref(false)
|
||||
const switchRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function toggleMenu() {
|
||||
menuVisible.value = !menuVisible.value
|
||||
}
|
||||
|
||||
function handleSelect(bl: BusinessLine) {
|
||||
if (!bl.authorized) return
|
||||
blStore.switchBL(bl.id)
|
||||
menuVisible.value = false
|
||||
ElMessage.success(`已切换业务线:${bl.name}(资源与告警按授权范围过滤)`)
|
||||
}
|
||||
|
||||
function onClickOutside(e: MouseEvent) {
|
||||
if (switchRef.value && !switchRef.value.contains(e.target as Node)) {
|
||||
menuVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bl-switch {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bl-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
background: var(--bg-panel-2);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
color: var(--text-hi);
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.bl-trigger:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.bl-trigger .bl-ic {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bl-trigger .caret {
|
||||
color: var(--text-dim);
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.bl-trigger .bl-role {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.bl-name-trigger {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.bl-menu {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
z-index: 60;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.bl-menu-label {
|
||||
font-size: 10px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 8px 10px 4px;
|
||||
}
|
||||
|
||||
.bl-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.bl-opt:hover {
|
||||
background: var(--bg-panel-2);
|
||||
}
|
||||
|
||||
.bl-opt.sel {
|
||||
background: var(--accent-dim);
|
||||
}
|
||||
|
||||
.bl-opt .bl-ic {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bl-opt .bl-name {
|
||||
font-size: 13px;
|
||||
color: var(--text-hi);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bl-opt .bl-sub {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.bl-opt .bl-check {
|
||||
margin-left: auto;
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bl-opt.sel .bl-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bl-opt.locked {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bl-opt.locked .bl-lock {
|
||||
margin-left: auto;
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 下拉动画 */
|
||||
.bl-menu-enter-active,
|
||||
.bl-menu-leave-active {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.bl-menu-enter-from,
|
||||
.bl-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
xinfra <span class="sub">统一基础设施平台</span>
|
||||
</div>
|
||||
<span class="env-pill">● 生产环境</span>
|
||||
<BusinessLineSwitcher />
|
||||
<div class="spacer"></div>
|
||||
<div class="search-box">
|
||||
<el-icon><Search /></el-icon>
|
||||
@@ -23,7 +24,7 @@
|
||||
<div class="avatar">{{ userInitial }}</div>
|
||||
<div>
|
||||
<div>{{ user?.display_name || '未登录' }}</div>
|
||||
<div class="ldap-tag">LDAP · {{ user?.business_line || '' }}业务线</div>
|
||||
<div class="ldap-tag">LDAP · {{ user?.business_line || '' }}</div>
|
||||
</div>
|
||||
<el-button class="logout-button" text @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
@@ -36,6 +37,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import BusinessLineSwitcher from '@/components/BusinessLineSwitcher.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const { logout } = useAuth()
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: string
|
||||
name: string
|
||||
ou: string
|
||||
role: string
|
||||
iconText: string
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
authorized: boolean
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'xinfra-current-bl'
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
const MOCK_BUSINESS_LINES: BusinessLine[] = [
|
||||
{
|
||||
id: 'las',
|
||||
name: 'LAS 业务线',
|
||||
ou: 'ou=las',
|
||||
role: 'SRE',
|
||||
iconText: 'LA',
|
||||
iconBg: '#1A1430',
|
||||
iconColor: '#C9A6FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'kodo',
|
||||
name: 'Kodo 业务线',
|
||||
ou: 'ou=kodo',
|
||||
role: '研发',
|
||||
iconText: 'KO',
|
||||
iconBg: '#0E1C2C',
|
||||
iconColor: '#8EC8FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'lingxi',
|
||||
name: '灵矽 业务线',
|
||||
ou: 'ou=lingxi',
|
||||
role: '未授权',
|
||||
iconText: 'LX',
|
||||
iconBg: '#241B0A',
|
||||
iconColor: '#FFC97A',
|
||||
authorized: false,
|
||||
},
|
||||
{
|
||||
id: 'ltoken',
|
||||
name: 'LTOKEN 业务线',
|
||||
ou: 'ou=ltoken',
|
||||
role: '研发',
|
||||
iconText: 'LT',
|
||||
iconBg: '#1A2A1A',
|
||||
iconColor: '#A6FFB0',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'maas',
|
||||
name: 'MAAS 业务线',
|
||||
ou: 'ou=maas',
|
||||
role: '未授权',
|
||||
iconText: 'MA',
|
||||
iconBg: '#2A1A2A',
|
||||
iconColor: '#FFB0E0',
|
||||
authorized: false,
|
||||
},
|
||||
]
|
||||
|
||||
function loadSavedBL(): BusinessLine | null {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved) return JSON.parse(saved)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>(MOCK_BUSINESS_LINES)
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
|
||||
// 默认选中第一个已授权的业务线
|
||||
if (!currentBL.value) {
|
||||
currentBL.value = businessLines.value.find((bl) => bl.authorized) || null
|
||||
}
|
||||
|
||||
const authorizedBLs = computed(() =>
|
||||
businessLines.value.filter((bl) => bl.authorized),
|
||||
)
|
||||
|
||||
function switchBL(id: string) {
|
||||
const bl = businessLines.value.find((b) => b.id === id)
|
||||
if (!bl || !bl.authorized) return
|
||||
currentBL.value = bl
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
}
|
||||
|
||||
return {
|
||||
businessLines,
|
||||
currentBL,
|
||||
authorizedBLs,
|
||||
switchBL,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user