添加个人中心及按角色展示内容
This commit is contained in:
@@ -63,8 +63,8 @@
|
||||
<!-- 内容展示区 (内部路由渲染) -->
|
||||
<view class="content-scroll">
|
||||
<view class="content-inner" :class="{ 'is-mobile': isMobile }">
|
||||
<slot></slot>
|
||||
<component :is="currentComponent" v-if="!isPageLoading && currentComponent != null"></component>
|
||||
<slot v-if="hasAccess"></slot>
|
||||
<component :is="currentComponent" v-if="hasAccess && !isPageLoading && currentComponent != null"></component>
|
||||
<AdminPageLoading v-if="isPageLoading"></AdminPageLoading>
|
||||
</view>
|
||||
<AdminFooter></AdminFooter>
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts'
|
||||
import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts'
|
||||
|
||||
const props = defineProps({
|
||||
currentPage: {
|
||||
@@ -131,6 +132,12 @@ const SUB_W = 200
|
||||
// 页面加载状态
|
||||
const isPageLoading = ref(false)
|
||||
|
||||
const hasAccess = computed<boolean>(() => {
|
||||
|
||||
|
||||
return hasAdminModuleAccess(activeTopMenuId.value)
|
||||
})
|
||||
|
||||
const hasNotification = ref<boolean>(false)
|
||||
|
||||
/**
|
||||
@@ -258,6 +265,8 @@ const breadcrumb = computed<Array<{id: string, title: string}>>(() => {
|
||||
|
||||
// 当前渲染的组件
|
||||
const currentComponent = computed<any>(() => {
|
||||
|
||||
|
||||
const route = findRouteById(activeRouteId.value)
|
||||
if (!route) return null
|
||||
return getComponent(route.componentKey)
|
||||
|
||||
@@ -25,24 +25,74 @@
|
||||
<view class="header-right">
|
||||
<view class="hbtn" @click="$emit('search')"><text>🔍</text></view>
|
||||
<view v-if="!isMobile" class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
|
||||
<view class="hbtn" @click="$emit('notify')">
|
||||
<view class="hbtn" @click="$emit('notify')">
|
||||
<text>🔔</text>
|
||||
<view class="dot" v-if="hasNotification"></view>
|
||||
</view>
|
||||
|
||||
<!-- 用户个人中心 / 下拉菜单 -->
|
||||
<view class="user-profile-container" @click="goToUserCenter">
|
||||
<text class="user-name">crmeb demo</text>
|
||||
<text class="user-arrow">▼</text>
|
||||
|
||||
<view class="user-dropdown" v-if="showUserMenu">
|
||||
<view class="dropdown-item" @click.stop="goToUserCenter">个人中心</view>
|
||||
<view class="dropdown-item" @click.stop="handleLogout">退出登录</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
toggleSubSider,
|
||||
showSubSider,
|
||||
layoutMode,
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
toggleSubSider,
|
||||
showSubSider,
|
||||
layoutMode,
|
||||
isOverlayVisible,
|
||||
isMobileMenuOpen
|
||||
isMobileMenuOpen,
|
||||
openRoute
|
||||
} from '@/layouts/admin/store/adminNavStore.uts'
|
||||
|
||||
const showUserMenu = ref(false)
|
||||
|
||||
function closeUserMenu() {
|
||||
showUserMenu.value = false
|
||||
}
|
||||
|
||||
function toggleUserMenu() {
|
||||
showUserMenu.value = !showUserMenu.value
|
||||
}
|
||||
|
||||
function goToUserCenter() {
|
||||
uni.showToast({ title: "尝试打开个人中心...", icon: "none", duration: 2000 });
|
||||
|
||||
uni.showToast({ title: "尝试打开个人中心...", icon: "none", duration: 2000 });
|
||||
|
||||
console.log("[AdminHeader] goToUserCenter called");
|
||||
console.log("[AdminHeader] goToUserCenter called");
|
||||
showUserMenu.value = false
|
||||
openRoute('home_user_center')
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
showUserMenu.value = false
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync('adminRole')
|
||||
uni.removeStorageSync('token')
|
||||
uni.reLaunch({
|
||||
url: '/pages/user/login'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
breadcrumb: Array<{id: string, title: string}>
|
||||
hasNotification: boolean
|
||||
@@ -82,6 +132,10 @@ const currentTitle = computed((): string => {
|
||||
|
||||
<style>
|
||||
.header{
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
height: 56px;
|
||||
background:#fff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
@@ -142,6 +196,12 @@ const currentTitle = computed((): string => {
|
||||
display: flex !important;
|
||||
}
|
||||
.header-right {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
@@ -151,7 +211,13 @@ const currentTitle = computed((): string => {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.header-right{
|
||||
.header-right{
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
align-items:center;
|
||||
@@ -176,4 +242,64 @@ const currentTitle = computed((): string => {
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.user-profile-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.user-arrow {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
z-index: 1002;
|
||||
z-index: 1002;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
width: 120px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.user-dropdown-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@ import PlaceholderPage from '@/layouts/admin/components/PlaceholderPage.uvue'
|
||||
|
||||
// 导入首页(内部组件,不包含 AdminLayout)
|
||||
import HomeIndex from '@/layouts/admin/pages/HomeIndex.uvue'
|
||||
import UserCenter from '@/pages/mall/admin/userCenter/index.uvue'
|
||||
|
||||
// --- 用户模块 ---
|
||||
import UserStatistic from '@/pages/mall/admin/user/statistics/index.uvue'
|
||||
@@ -179,6 +180,7 @@ import MaintainSysInfo from '@/pages/mall/admin/maintain/sys/info.uvue'
|
||||
export const componentMap: Map<string, any> = new Map([
|
||||
// 首页
|
||||
['HomeIndex', HomeIndex],
|
||||
['UserCenter', UserCenter],
|
||||
|
||||
// 用户模块
|
||||
['UserStatistic', UserStatistic],
|
||||
|
||||
@@ -29,6 +29,8 @@ export type RouteRecord = {
|
||||
/**
|
||||
* 菜单分组类型
|
||||
*/
|
||||
import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts'
|
||||
|
||||
export type MenuGroup = {
|
||||
id: string
|
||||
title: string
|
||||
@@ -232,8 +234,17 @@ export const routes: RouteRecord[] = [
|
||||
},
|
||||
|
||||
// ========== 用户模块 ==========
|
||||
{
|
||||
id: 'user_statistic',
|
||||
// ========== 个人中心 ==========
|
||||
{
|
||||
id: 'home_user_center',
|
||||
title: '个人中心',
|
||||
path: '/pages/mall/admin/userCenter/index',
|
||||
componentKey: 'UserCenter',
|
||||
order: 100
|
||||
},
|
||||
|
||||
{
|
||||
id: 'user_statistic',
|
||||
title: '用户统计',
|
||||
path: '/pages/mall/admin/user/statistics/index',
|
||||
componentKey: 'UserStatistic',
|
||||
@@ -1175,7 +1186,10 @@ export const routes: RouteRecord[] = [
|
||||
* 获取所有一级菜单
|
||||
*/
|
||||
export function getTopMenus(): TopMenu[] {
|
||||
return topMenus.sort((a, b) => a.order - b.order)
|
||||
// 基于 role 的模块过滤
|
||||
return topMenus
|
||||
.filter(m => hasAdminModuleAccess(m.id))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getTopMenus
|
||||
} from '@/layouts/admin/router/adminRoutes.uts'
|
||||
import { addView, activeFullPath, visitedViews } from './tagsViewStore.uts'
|
||||
import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts'
|
||||
|
||||
/**
|
||||
* 标签页类型
|
||||
@@ -77,14 +78,34 @@ export const isOverlayVisible = ref<boolean>(false)
|
||||
* @param addTab 是否添加到标签页
|
||||
*/
|
||||
export function openRoute(routeId: string, addTab: boolean = true): void {
|
||||
|
||||
|
||||
const route = findRouteById(routeId)
|
||||
if (!route) {
|
||||
console.warn(`[AdminNav] Route not found: ${routeId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// 基于 role 的页面访问拦截
|
||||
// route.parentId 对应上方 topMenus 的 id。这里校验是否有权限
|
||||
const moduleId = route.parentId ? route.parentId : route.id.split('_')[0]
|
||||
if (!hasAdminModuleAccess(moduleId)) {
|
||||
uni.showToast({
|
||||
title: '您没有权限访问该模块',
|
||||
icon: 'none'
|
||||
})
|
||||
console.warn(`[AdminNav] Access denied for role to module: ${moduleId}`)
|
||||
// 回退到首页
|
||||
if (routeId !== 'home_index') {
|
||||
openRoute('home_index', addTab)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 更新当前路由
|
||||
activeRouteId.value = routeId
|
||||
|
||||
|
||||
|
||||
// 更新一级菜单选中态
|
||||
if (route.parentId) {
|
||||
|
||||
51
layouts/admin/utils/role.uts
Normal file
51
layouts/admin/utils/role.uts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Admin role-based access control
|
||||
export function getCurrentAdminRole(): string {
|
||||
// 从本地存储获取当前 role。为不影响全局,优先读 admin_role,默认降级到读全局 role/userInfo,再默认
|
||||
// 根据实际系统中的 token/userInfo 结构可灵活调整
|
||||
const roleType = uni.getStorageSync('admin_role') as string
|
||||
if (roleType && typeof roleType === 'string' && roleType.length > 0) {
|
||||
return roleType
|
||||
}
|
||||
|
||||
// 检查是否有关联 merchant_id 或者是存了全局 role
|
||||
const merchantId = uni.getStorageSync('merchant_id')
|
||||
if (merchantId) return 'merchant'
|
||||
|
||||
const globalRole = uni.getStorageSync('role') as string
|
||||
if (globalRole && typeof globalRole === 'string' && globalRole.length > 0) {
|
||||
return globalRole
|
||||
}
|
||||
|
||||
return 'admin' // 默认返回 admin 作为兜底(兼容原有全部显示逻辑)
|
||||
}
|
||||
|
||||
// 获取不同 role 允许访问的顶级模块 ID (主侧边栏的顶层菜单 id)
|
||||
export function getVisibleTopMenuIds(role: string): string[] {
|
||||
if (role === 'admin') {
|
||||
return ['home', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain']
|
||||
}
|
||||
|
||||
if (role === 'merchant') {
|
||||
// merchant: 只能看到 主页、订单、商品、营销、财务
|
||||
return ['home', 'order', 'product', 'marketing', 'finance']
|
||||
}
|
||||
|
||||
// 其他 role: 安全兜底
|
||||
return ['home']
|
||||
}
|
||||
|
||||
// 判断是否有权限访问某模块或路由
|
||||
export function hasAdminModuleAccess(moduleId: string | undefined): boolean {
|
||||
if (!moduleId) return true // 没有指定的通常认为是公共的,比如一些通用组件
|
||||
|
||||
const role = getCurrentAdminRole()
|
||||
if (role === 'admin') return true
|
||||
|
||||
// 对于 merchant 角色,允许其访问的顶级 menu id 及其所属路由
|
||||
if (role === 'merchant') {
|
||||
const allowed = ['home', 'order', 'product', 'marketing', 'finance']
|
||||
return allowed.includes(moduleId)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user