修正了公司定制内容隔离
This commit is contained in:
1 parent
15bcb072f9
commit
917d4d1d28
7 files changed
+564
-63
No files matched your search
+3
-3
@@ -55,9 +55,9 @@ export const getUserRoles = (): Promise<RoleItem[]> => {
|
||||
return get<{ roles: RoleItem[] }>('/Auth/GetUserRoles').then((res) => res.roles || [])
|
||||
}
|
||||
|
||||
/** 切换当前角色 */
|
||||
export const switchRole = (role: string): Promise<void> => {
|
||||
return post<void>('/Auth/SwitchRole', { Role: role })
|
||||
/** 切换当前角色(后端契约:JSON body 传 RoleUUID) */
|
||||
export const switchRole = (role: RoleItem): Promise<void> => {
|
||||
return post<void>('/Auth/SwitchRole', { RoleUUID: role.RoleUUID })
|
||||
}
|
||||
|
||||
/** 获取当前登录用户信息(silent=true 时失败不弹全局提示,用于登录页会话恢复) */
|
||||
|
||||
+7
-1
@@ -8,6 +8,8 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { ApiResult } from '@/types/api'
|
||||
import { getServerScopeParams } from '@/utils/serverScope'
|
||||
import router from '@/router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
|
||||
/** 后端响应统一格式 */
|
||||
type BackendResponse<T> = AxiosResponse<ApiResult<T>>
|
||||
@@ -113,7 +115,11 @@ function handleLoginExpired(): void {
|
||||
confirmButtonText: '重新登录',
|
||||
})
|
||||
.then(() => {
|
||||
// 清除本地用户状态
|
||||
// 清除本地用户状态(内存态 + localStorage)
|
||||
// 必须清内存态:登录页守卫依据 userStore.isLoggedIn 判断,若不清内存,
|
||||
// 点"重新登录"会被守卫重定向回 /dashboard,表现为跳不到登录页
|
||||
useUserStore().reset()
|
||||
useTabsStore().reset()
|
||||
localStorage.removeItem('gerp-user')
|
||||
// 使用 Vue Router 跳转登录页
|
||||
// hash 路由模式下 window.location.href 会保留 #/path,导致 Vue Router 按 hash 误路由到业务页("直接显示后台")
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 切换角色对话框
|
||||
*
|
||||
* 仿原版 GERP 的 ShowRoleSwitcher 弹窗:
|
||||
* - 全屏遮罩 + 居中白色卡片,入场缩放+上移动画
|
||||
* - 角色网格卡片布局(两列),每张卡片显示角色名 + 公司名(简称)
|
||||
* - 当前角色带"当前"徽章,禁用角色带"已禁用"徽章且不可选
|
||||
* - 点击卡片选中(右上角对勾),确认切换后调用 SwitchRole 接口
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { RoleItem } from '@/api/auth'
|
||||
import { switchRole } from '@/api/auth'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps<{
|
||||
/** 是否显示 */
|
||||
modelValue: boolean
|
||||
/** 角色列表 */
|
||||
roles: RoleItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'switched', role: RoleItem): void
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const tabsStore = useTabsStore()
|
||||
const router = useRouter()
|
||||
|
||||
const selectedRole = ref<RoleItem | null>(null)
|
||||
const switching = ref(false)
|
||||
|
||||
/** 角色是否启用(RoleStatus === 1 为启用) */
|
||||
const isEnabled = (role: RoleItem): boolean => role.RoleStatus === 1 || role.RoleStatus === '1'
|
||||
|
||||
/** 选中一张角色卡片(当前角色/禁用角色不可选) */
|
||||
function selectRole(role: RoleItem): void {
|
||||
if (role.active || !isEnabled(role)) return
|
||||
selectedRole.value = role
|
||||
}
|
||||
|
||||
/** 关闭对话框 */
|
||||
function close(): void {
|
||||
emit('update:modelValue', false)
|
||||
selectedRole.value = null
|
||||
}
|
||||
|
||||
/** 确认切换 */
|
||||
async function confirm(): Promise<void> {
|
||||
if (!selectedRole.value || switching.value) return
|
||||
switching.value = true
|
||||
try {
|
||||
await switchRole(selectedRole.value)
|
||||
// 切换成功后刷新用户信息与菜单(不同角色可能有不同菜单权限)
|
||||
await userStore.fetchUserInfo()
|
||||
await userStore.fetchMenus()
|
||||
await userStore.fetchRoles()
|
||||
tabsStore.reset()
|
||||
ElMessage.success(`已切换为 "${selectedRole.value.RoleName}"`)
|
||||
emit('switched', selectedRole.value)
|
||||
close()
|
||||
router.push('/dashboard')
|
||||
} catch {
|
||||
ElMessage.error('身份切换失败,请重试')
|
||||
} finally {
|
||||
switching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 角色描述:公司全名(公司简称),无公司时显示"无所属公司" */
|
||||
function roleDesc(role: RoleItem): string {
|
||||
if (role.CompanyName) {
|
||||
return role.CompanyShort ? `${role.CompanyName} (${role.CompanyShort})` : role.CompanyName
|
||||
}
|
||||
return '无所属公司'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="role-switcher">
|
||||
<div v-if="modelValue" class="role-switcher" @click.self="close">
|
||||
<div class="role-switcher-overlay" @click="close" />
|
||||
<div class="role-switcher-content">
|
||||
<!-- 头部 -->
|
||||
<div class="role-switcher-header">
|
||||
<h3 class="role-switcher-title">切换角色</h3>
|
||||
<button class="role-switcher-close" @click="close">
|
||||
<svg viewBox="0 0 1024 1024" width="14" height="14"><path fill="currentColor" d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 角色卡片网格 -->
|
||||
<div class="role-switcher-body">
|
||||
<div class="role-grid">
|
||||
<div
|
||||
v-for="role in roles"
|
||||
:key="role.RoleUUID"
|
||||
class="role-card"
|
||||
:class="{
|
||||
selected: selectedRole?.RoleUUID === role.RoleUUID,
|
||||
disabled: !isEnabled(role),
|
||||
}"
|
||||
:title="!isEnabled(role) ? '该角色已禁用' : undefined"
|
||||
@click="selectRole(role)"
|
||||
>
|
||||
<div class="role-icon">
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20"><path fill="currentColor" d="M482.9 339.2c-42.3 0-76.8-34.5-76.8-76.8s34.5-76.8 76.8-76.8 76.8 34.5 76.8 76.8-34.5 76.8-76.8 76.8z m0-102.4c-14.1 0-25.6 11.5-25.6 25.6s11.5 25.6 25.6 25.6 25.6-11.5 25.6-25.6-11.5-25.6-25.6-25.6zM512 896c-11.8 0-23.6-4.5-32.6-13.5L332.6 735.7c-72.8-72.8-72.8-191.2 0-264l18-18c35.4-35.4 93.3-35.4 128.7 0l32.7 32.7 32.7-32.7c35.4-35.4 93.3-35.4 128.7 0l18 18c72.8 72.8 72.8 191.2 0 264l-146.8 146.8c-9 9-20.8 13.5-32.6 13.5z m0-43.8 137-137c50-50 50-131.4 0-181.4l-18-18c-8.8-8.8-20.6-13.2-32.2-13.2s-23.4 4.4-32.2 13.2L512 571.2l-54.6-54.6c-8.8-8.8-20.6-13.2-32.2-13.2s-23.4 4.4-32.2 13.2l-18 18c-50 50-50 131.4 0 181.4z" /></svg>
|
||||
</div>
|
||||
<div class="role-info">
|
||||
<div class="role-name">{{ role.RoleName }}</div>
|
||||
<div class="role-desc">{{ roleDesc(role) }}</div>
|
||||
</div>
|
||||
<!-- 当前角色徽章 -->
|
||||
<span v-if="role.active" class="role-badge role-badge-active">当前</span>
|
||||
<!-- 禁用角色徽章 -->
|
||||
<span v-else-if="!isEnabled(role)" class="role-badge role-badge-disabled">已禁用</span>
|
||||
<!-- 选中对勾 -->
|
||||
<div v-if="selectedRole?.RoleUUID === role.RoleUUID" class="role-check">
|
||||
<svg viewBox="0 0 1024 1024" width="12" height="12"><path fill="currentColor" d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="role-switcher-footer">
|
||||
<button class="gerp-btn gerp-btn-secondary" @click="close">取消</button>
|
||||
<button
|
||||
class="gerp-btn gerp-btn-primary"
|
||||
:disabled="!selectedRole || switching"
|
||||
@click="confirm"
|
||||
>
|
||||
<span v-if="switching" class="role-switching">
|
||||
<svg viewBox="0 0 1024 1024" width="14" height="14" class="is-loading"><path fill="currentColor" d="M512 64a448 448 0 1 0 0 896 448 448 0 0 0 0-896z m0 64a384 384 0 1 1 0 768 384 384 0 0 1 0-768z" /></svg>
|
||||
切换中...
|
||||
</span>
|
||||
<span v-else>确认切换</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 全屏容器 + 入场淡入 */
|
||||
.role-switcher {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.role-switcher-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.role-switcher-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 720px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 入场动画:缩放 + 上移 */
|
||||
.role-switcher-enter-active,
|
||||
.role-switcher-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.role-switcher-enter-active .role-switcher-content,
|
||||
.role-switcher-leave-active .role-switcher-content {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.role-switcher-enter-from,
|
||||
.role-switcher-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.role-switcher-enter-from .role-switcher-content,
|
||||
.role-switcher-leave-to .role-switcher-content {
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.role-switcher-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
.role-switcher-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.role-switcher-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.role-switcher-close:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
/* 角色网格 */
|
||||
.role-switcher-body {
|
||||
padding: 20px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.role-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.role-card {
|
||||
width: calc(50% - 6px);
|
||||
min-width: 240px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--el-fill-color-light);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.role-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
.role-card.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.role-card.disabled:hover {
|
||||
transform: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* 选中高亮 */
|
||||
.role-card.selected {
|
||||
border-color: var(--el-color-primary);
|
||||
background: rgba(64, 158, 255, 0.08);
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
background: rgba(64, 158, 255, 0.12);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.role-card:hover .role-icon {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
min-width: 0;
|
||||
}
|
||||
.role-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.role-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 徽章 */
|
||||
.role-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
.role-badge-active {
|
||||
background: var(--el-color-primary);
|
||||
}
|
||||
.role-badge-disabled {
|
||||
background: var(--el-text-color-disabled);
|
||||
}
|
||||
|
||||
/* 选中对勾 */
|
||||
.role-check {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-color-primary);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
animation: role-check-in 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@keyframes role-check-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.role-switcher-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.gerp-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.gerp-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.gerp-btn-secondary {
|
||||
background: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
.gerp-btn-secondary:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.gerp-btn-primary {
|
||||
background: var(--el-color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.gerp-btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.role-switching {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.is-loading {
|
||||
animation: role-spin 1s linear infinite;
|
||||
}
|
||||
@keyframes role-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,14 +10,14 @@
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useTabsStore } from '@/stores/tabs'
|
||||
import { getAvatarUrl, type RoleItem } from '@/api/auth'
|
||||
import { getAvatarUrl } from '@/api/auth'
|
||||
import TabBar from '@/components/TabBar.vue'
|
||||
import RouteProgress from '@/components/RouteProgress.vue'
|
||||
import PageMask from '@/components/PageMask.vue'
|
||||
import RoleSwitcher from '@/components/RoleSwitcher.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
@@ -42,12 +42,12 @@ const roleLabel = computed(() => {
|
||||
})
|
||||
|
||||
// === 身份切换 ===
|
||||
const roleSwitching = ref(false)
|
||||
const roles = computed(() => userStore.roles)
|
||||
const roleSwitcherVisible = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
appStore.initTheme()
|
||||
// 获取角色列表(用于身份切换)
|
||||
// 获取角色列表(用于切换身份对话框)
|
||||
if (userStore.isLoggedIn) {
|
||||
userStore.fetchRoles().catch(() => {
|
||||
// 获取角色失败静默处理
|
||||
@@ -55,20 +55,13 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/** 切换身份/角色(role 为 RoleItem 对象) */
|
||||
async function handleSwitchRole(role: RoleItem): Promise<void> {
|
||||
if (role.RoleName === userStore.currentRole || roleSwitching.value) return
|
||||
roleSwitching.value = true
|
||||
try {
|
||||
await userStore.switchRole(role)
|
||||
tabsStore.reset()
|
||||
ElMessage.success(`已切换到:${role.RoleName}`)
|
||||
router.push('/dashboard')
|
||||
} catch {
|
||||
ElMessage.error('身份切换失败')
|
||||
} finally {
|
||||
roleSwitching.value = false
|
||||
}
|
||||
/** 打开切换角色对话框 */
|
||||
function openRoleSwitcher(): void {
|
||||
// 打开前刷新角色列表,保证最新
|
||||
userStore.fetchRoles().catch(() => {
|
||||
// 静默处理
|
||||
})
|
||||
roleSwitcherVisible.value = true
|
||||
}
|
||||
|
||||
function handleMenuSelect(index: string): void {
|
||||
@@ -122,6 +115,9 @@ function handleUserCommand(cmd: string): void {
|
||||
case 'settings':
|
||||
router.push('/system/settings')
|
||||
break
|
||||
case 'switchRole':
|
||||
openRoleSwitcher()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,27 +210,10 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
<div v-if="userInfo?.UserName" class="user-dropdown-account">@{{ userInfo.UserName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 身份切换区域(仅多角色用户显示) -->
|
||||
<div v-if="roles.length > 1" class="user-dropdown-roles">
|
||||
<div class="user-dropdown-roles-title">
|
||||
<el-icon><Switch /></el-icon>
|
||||
<span>切换身份</span>
|
||||
</div>
|
||||
<div class="user-dropdown-roles-list">
|
||||
<div
|
||||
v-for="role in roles"
|
||||
:key="role.RoleUUID || role.RoleName"
|
||||
class="user-dropdown-role-item"
|
||||
:class="{ active: role.RoleName === userStore.currentRole }"
|
||||
@click="handleSwitchRole(role)"
|
||||
>
|
||||
<el-icon v-if="role.RoleName === userStore.currentRole" class="role-check"><Check /></el-icon>
|
||||
<el-icon v-else class="role-placeholder"><CircleCheck /></el-icon>
|
||||
<span>{{ role.RoleName }}</span>
|
||||
<el-icon v-if="roleSwitching" class="is-loading role-loading"><Loading /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 切换角色:打开独立对话框(仿原版 GERP) -->
|
||||
<el-dropdown-item command="switchRole">
|
||||
<el-icon><Switch /></el-icon>切换角色
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided command="profile">
|
||||
<el-icon><User /></el-icon>个人中心
|
||||
</el-dropdown-item>
|
||||
@@ -329,6 +308,9 @@ const isFaIcon = (icon: string | undefined | null): boolean => {
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 切换角色对话框(仿原版 GERP) -->
|
||||
<RoleSwitcher v-model="roleSwitcherVisible" :roles="roles" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+56
-16
@@ -34,13 +34,13 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
path: 'workorder/admin',
|
||||
name: 'WorkorderAdmin',
|
||||
component: () => import('@/views/workorder/Admin.vue'),
|
||||
meta: { title: '工单管理', icon: 'Tickets', requiresAuth: true },
|
||||
meta: { title: '工单管理', icon: 'Tickets', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'workorder/my',
|
||||
name: 'WorkorderMy',
|
||||
component: () => import('@/views/workorder/My.vue'),
|
||||
meta: { title: '我的工单', icon: 'Document', requiresAuth: true },
|
||||
meta: { title: '我的工单', icon: 'Document', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/roles',
|
||||
@@ -58,44 +58,44 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
path: 'inventory/products',
|
||||
name: 'InventoryProducts',
|
||||
component: () => import('@/views/inventory/Products.vue'),
|
||||
meta: { title: '产品管理', icon: 'Goods', requiresAuth: true },
|
||||
meta: { title: '产品管理', icon: 'Goods', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'sales/orders',
|
||||
name: 'SalesOrders',
|
||||
component: () => import('@/views/sales/Orders.vue'),
|
||||
meta: { title: '销售订单', icon: 'ShoppingCart', requiresAuth: true },
|
||||
meta: { title: '销售订单', icon: 'ShoppingCart', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
// === 阶段 2:Company 列表页 ===
|
||||
{
|
||||
path: 'company/tenant',
|
||||
name: 'CompanyTenant',
|
||||
component: () => import('@/views/company/Tenant.vue'),
|
||||
meta: { title: '租户管理', icon: 'OfficeBuilding', requiresAuth: true },
|
||||
meta: { title: '租户管理', icon: 'OfficeBuilding', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'company/company',
|
||||
name: 'CompanyCompany',
|
||||
component: () => import('@/views/company/Company.vue'),
|
||||
meta: { title: '公司管理', icon: 'Management', requiresAuth: true },
|
||||
meta: { title: '公司管理', icon: 'Management', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'company/department',
|
||||
name: 'CompanyDepartment',
|
||||
component: () => import('@/views/company/Department.vue'),
|
||||
meta: { title: '部门管理', icon: 'Share', requiresAuth: true },
|
||||
meta: { title: '部门管理', icon: 'Share', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'company/workgroup',
|
||||
name: 'CompanyWorkgroup',
|
||||
component: () => import('@/views/company/Workgroup.vue'),
|
||||
meta: { title: '工作组管理', icon: 'UserFilled', requiresAuth: true },
|
||||
meta: { title: '工作组管理', icon: 'UserFilled', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'company/role',
|
||||
name: 'CompanyRole',
|
||||
component: () => import('@/views/company/Role.vue'),
|
||||
meta: { title: '公司角色', icon: 'Avatar', requiresAuth: true },
|
||||
meta: { title: '公司角色', icon: 'Avatar', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
// === 阶段 3:详情页 ===
|
||||
{
|
||||
@@ -158,19 +158,19 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
path: 'system/message',
|
||||
name: 'SystemMessage',
|
||||
component: () => import('@/views/system/Message.vue'),
|
||||
meta: { title: '站内信', icon: 'ChatDotRound', requiresAuth: true },
|
||||
meta: { title: '站内信', icon: 'ChatDotRound', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/tenant-create',
|
||||
name: 'SystemTenantCreate',
|
||||
component: () => import('@/views/system/TenantCreate.vue'),
|
||||
meta: { title: '租户入驻', requiresAuth: true },
|
||||
meta: { title: '租户入驻', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/tenant-review',
|
||||
name: 'SystemTenantReview',
|
||||
component: () => import('@/views/system/TenantReview.vue'),
|
||||
meta: { title: '租户审核', requiresAuth: true },
|
||||
meta: { title: '租户审核', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/payment-config',
|
||||
@@ -189,13 +189,13 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
path: 'doc/editor',
|
||||
name: 'DocEditor',
|
||||
component: () => import('@/views/doc/Editor.vue'),
|
||||
meta: { title: '文档编辑器', requiresAuth: true },
|
||||
meta: { title: '文档编辑器', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'assets/manager',
|
||||
name: 'AssetsManager',
|
||||
component: () => import('@/views/assets/Manager.vue'),
|
||||
meta: { title: '资产管理', requiresAuth: true },
|
||||
meta: { title: '资产管理', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
// === 阶段 7:管理员工具 ===
|
||||
{
|
||||
@@ -214,13 +214,13 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
path: 'system/menu',
|
||||
name: 'SystemMenu',
|
||||
component: () => import('@/views/system/Menu.vue'),
|
||||
meta: { title: '菜单管理', requiresAuth: true },
|
||||
meta: { title: '菜单管理', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/database',
|
||||
name: 'SystemDatabase',
|
||||
component: () => import('@/views/system/Database.vue'),
|
||||
meta: { title: '数据库管理', requiresAuth: true },
|
||||
meta: { title: '数据库管理', requiresAuth: true, menuGuarded: true },
|
||||
},
|
||||
{
|
||||
path: 'system/dbtable',
|
||||
@@ -228,6 +228,37 @@ export const staticRoutes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/system/DbTable.vue'),
|
||||
meta: { title: '表结构管理', requiresAuth: true },
|
||||
},
|
||||
// === 枢机协律(CreateE):旧页面 iframe 直载(LegacyPage) ===
|
||||
{
|
||||
path: 'private/CreateE/shop',
|
||||
name: 'PrivateCreateEShop',
|
||||
component: () => import('@/views/LegacyPage.vue'),
|
||||
meta: { title: '商店', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/shop.html' },
|
||||
},
|
||||
{
|
||||
path: 'private/CreateE/Recharge',
|
||||
name: 'PrivateCreateERecharge',
|
||||
component: () => import('@/views/LegacyPage.vue'),
|
||||
meta: { title: '储值充值', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/Recharge.html' },
|
||||
},
|
||||
{
|
||||
path: 'private/CreateE/wallet',
|
||||
name: 'PrivateCreateEWallet',
|
||||
component: () => import('@/views/LegacyPage.vue'),
|
||||
meta: { title: '钱包', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/wallet.html' },
|
||||
},
|
||||
{
|
||||
path: 'private/CreateE/enterprise',
|
||||
name: 'PrivateCreateEEnterprise',
|
||||
component: () => import('@/views/LegacyPage.vue'),
|
||||
meta: { title: '企业管理', requiresAuth: true, legacySrc: '/pages/private/CreateE/enterprise.html' },
|
||||
},
|
||||
{
|
||||
path: 'private/CreateE/StoreValue',
|
||||
name: 'PrivateCreateEStoreValue',
|
||||
component: () => import('@/views/LegacyPage.vue'),
|
||||
meta: { title: '储值查看', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/StoreValue.html' },
|
||||
},
|
||||
// === 阶段 8:SKU(移动端 C 端,待决策是否拆分独立工程) ===
|
||||
{
|
||||
path: 'sku',
|
||||
@@ -322,6 +353,15 @@ router.beforeEach(async (to, _from, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单守卫:menuGuarded 路由(后端菜单驱动的业务页)必须出现在当前用户可见菜单树中
|
||||
// 用途:URL 直达私有页(如 #/private/CreateE/wallet)时,若菜单树不含该路径(当前角色
|
||||
// 公司无权),则拦截到 404,避免绕过菜单过滤直接访问公司私有业务
|
||||
if (to.meta.menuGuarded && !userStore.hasMenuPath(to.path)) {
|
||||
tabsStore.removeTab(to.path)
|
||||
next({ name: 'NotFound', replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
+17
-4
@@ -69,6 +69,20 @@ export const useUserStore = defineStore('user', () => {
|
||||
return result
|
||||
})
|
||||
|
||||
/** 目标路径是否在用户可见菜单树中(含子菜单;供路由守卫校验 URL 直达私有页) */
|
||||
function hasMenuPath(path: string): boolean {
|
||||
if (!path) return false
|
||||
const normalized = path.split('?')[0].replace(/\/+$/, '')
|
||||
const walk = (items: MenuItem[]): boolean => {
|
||||
for (const item of items) {
|
||||
if (item.Path && item.Path === normalized) return true
|
||||
if (item.Children?.length && walk(item.Children)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return walk(menus.value)
|
||||
}
|
||||
|
||||
// === 动作 ===
|
||||
/** 登录 */
|
||||
async function login(params: LoginParams): Promise<void> {
|
||||
@@ -135,10 +149,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
|
||||
/** 切换当前角色(身份切换) */
|
||||
async function switchRole(role: RoleItem | string): Promise<void> {
|
||||
// 传对象时取 RoleName(后端 SwitchRole 期望角色名)
|
||||
const roleName = typeof role === 'string' ? role : role.RoleName
|
||||
await apiSwitchRole(roleName)
|
||||
async function switchRole(role: RoleItem): Promise<void> {
|
||||
await apiSwitchRole(role)
|
||||
// 刷新用户信息和菜单(不同角色可能有不同菜单权限)
|
||||
await fetchUserInfo()
|
||||
await fetchMenus()
|
||||
@@ -198,6 +210,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
avatar,
|
||||
displayName,
|
||||
flatMenus,
|
||||
hasMenuPath,
|
||||
// 动作
|
||||
login,
|
||||
logout,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* LegacyPage
|
||||
*
|
||||
* 通用旧页面承载组件:以 iframe 直载 TempLate/pages/ 下的原生 HTML 页面。
|
||||
* 用于尚未迁移到 Vue 的历史菜单(如"枢机协律"系列的商店/钱包/充值等)。
|
||||
* 通过路由 meta.legacySrc 指定 iframe 加载的原始 HTML 路径。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
// 旧页面原始路径(如 /pages/private/CreateE/shop.html),由路由 meta 传入
|
||||
const src = computed(() => (route.meta.legacySrc as string) || '')
|
||||
const loading = ref(true)
|
||||
|
||||
function onLoad(): void {
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="legacy-page">
|
||||
<el-skeleton v-if="loading" :rows="6" animated class="legacy-page-skeleton" />
|
||||
<iframe
|
||||
v-show="!loading"
|
||||
:src="src"
|
||||
class="legacy-page-frame"
|
||||
frameborder="0"
|
||||
@load="onLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.legacy-page {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--gerp-bg-color, #f5f7fa);
|
||||
}
|
||||
|
||||
.legacy-page-skeleton {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.legacy-page-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user