Files
medical-mall/layouts/admin/index.uvue

642 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="admin-layout">
<!-- 主侧边栏 -->
<view class="admin-sider" :class="{ 'sider-collapsed': isCollapsed }">
<!-- Logo区域 -->
<view class="sider-header">
<view class="logo">
<image class="logo-img" src="/static/logo.png" mode="aspectFit" />
<text class="logo-text" v-if="!isCollapsed">商城后台</text>
</view>
<view class="collapse-btn" @click="toggleCollapse">
<text class="iconfont">{{ isCollapsed ? 'icon-menu-unfold' : 'icon-menu-fold' }}</text>
</view>
</view>
<!-- 一级菜单(图标 + 文案;折叠时只显示图标) -->
<scroll-view class="menu-primary" scroll-y="true">
<view
v-for="menu in menuList"
:key="menu.id"
class="menu-item-primary"
:class="{ active: activeMenu === menu.id }"
@click="handleMenuClick(menu)"
>
<image class="menu-icon-img" :src="menu.icon" mode="aspectFit" />
<text class="menu-title" v-if="!isCollapsed">{{ menu.title }}</text>
</view>
</scroll-view>
</view>
<!-- 主内容区:二级侧边栏 + 右侧内容 -->
<view class="admin-main" :class="{ 'main-collapsed': isCollapsed }">
<!-- 二级侧边栏(分组) -->
<view class="content-sider" v-if="activeGroups.length > 0">
<view class="content-sider-header">
<text class="content-sider-title">{{ activeMenuTitle }}</text>
</view>
<scroll-view class="content-sider-scroll" scroll-y="true">
<view class="group-wrap">
<view v-for="group in activeGroups" :key="group.id" class="group-block">
<view class="group-header" @click="toggleGroup(group.id)">
<text class="group-title">{{ group.title }}</text>
<text class="group-arrow">{{ isGroupOpen(group.id) ? '▾' : '▸' }}</text>
</view>
<view v-if="isGroupOpen(group.id)" class="group-children">
<view
v-for="sub in group.children"
:key="sub.id"
class="content-sub-menu-item"
:class="{ active: activeSubMenu === sub.id }"
@click="handleSubMenuClick(sub, group.id)"
>
<text class="content-sub-menu-text">{{ sub.title }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 右侧内容区 -->
<view class="content-area">
<!-- 顶部 Header -->
<view class="admin-header">
<view class="header-left">
<view class="breadcrumb">
<text class="breadcrumb-item">{{ activeMenuTitle }}</text>
<text class="breadcrumb-separator" v-if="activeSubMenuTitle"> / </text>
<text class="breadcrumb-item active" v-if="activeSubMenuTitle">{{ activeSubMenuTitle }}</text>
</view>
</view>
<view class="header-right">
<view class="header-action" @click="handleSearch">
<text class="iconfont icon-search"></text>
</view>
<view class="header-action" @click="handleNotification">
<text class="iconfont icon-bell"></text>
<view class="notification-dot" v-if="hasNotification"></view>
</view>
<view class="header-action" @click="handleFullscreen">
<text class="iconfont icon-fullscreen"></text>
</view>
<view class="header-user" @click="handleUserMenu">
<image class="user-avatar" src="/static/avatar/default.png" mode="aspectFill" />
<text class="user-name">管理员</text>
<text class="iconfont icon-down"></text>
</view>
</view>
</view>
<!-- 页面内容区 -->
<scroll-view class="page-content" scroll-y="true">
<view class="page-wrapper">
<slot></slot>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
type SubMenu = { id: string; title: string; path: string }
type MenuGroup = { id: string; title: string; children: SubMenu[] }
type MenuItem = { id: string; title: string; icon: string; path: string; groups?: MenuGroup[] }
const props = defineProps<{
currentPage: string
}>()
const isCollapsed = ref(false)
const activeMenu = ref('home')
const activeSubMenu = ref('')
const hasNotification = ref(true)
// 当前激活菜单下:展开的分组
const openGroupIds = ref<string[]>([])
// ✅ 你 static 里 6 个 svghomepage / user / shopping / order / statistics / setting
// 菜单多于 6 个时允许复用(财务、统计都用 statistics.svg
const menuList = ref<MenuItem[]>([
{
id: 'home',
title: '首页',
icon: '/static/homepage.svg',
path: '/pages/mall/admin/index',
groups: []
},
{
id: 'user',
title: '用户',
icon: '/static/user.svg',
path: '/pages/mall/admin/user-management',
groups: [
{
id: 'user-group',
title: '用户管理',
children: [
{ id: 'user-list', title: '用户列表', path: '/pages/mall/admin/user-management' },
{ id: 'user-add', title: '添加用户', path: '/pages/mall/admin/user-management?action=add' }
]
}
]
},
{
id: 'product',
title: '商品',
icon: '/static/shopping.svg',
path: '/pages/mall/admin/product-management',
groups: [
{
id: 'product-group',
title: '商品管理',
children: [
{ id: 'product-list', title: '商品列表', path: '/pages/mall/admin/product-management' },
{ id: 'product-add', title: '添加商品', path: '/pages/mall/admin/product-management?action=add' },
{ id: 'category', title: '商品分类', path: '/pages/mall/admin/product-management?tab=category' }
]
}
]
},
{
id: 'order',
title: '订单',
icon: '/static/order.svg',
path: '/pages/mall/admin/order-management',
groups: [
{
id: 'order-group',
title: '订单管理',
children: [
{ id: 'order-list', title: '订单列表', path: '/pages/mall/admin/order-management' },
{ id: 'order-detail', title: '订单详情', path: '/pages/mall/admin/order-management?action=detail' }
]
}
]
},
{
id: 'finance',
title: '财务',
icon: '/static/finance.svg',
path: '/pages/mall/admin/finance-management',
groups: [
{
id: 'finance-group',
title: '财务管理',
children: [
{ id: 'finance-overview', title: '财务概览', path: '/pages/mall/admin/finance-management' },
{ id: 'withdrawals', title: '提现管理', path: '/pages/mall/admin/finance-management?tab=withdrawals' }
]
}
]
},
{
id: 'statistics',
title: '统计',
icon: '/static/statistics.svg',
path: '/pages/mall/admin/user-statistics',
groups: []
},
{
id: 'system',
title: '设置',
icon: '/static/setting.svg',
path: '/pages/mall/admin/system-settings',
groups: [
{
id: 'system-group',
title: '系统设置',
children: [
{ id: 'basic', title: '基本设置', path: '/pages/mall/admin/system-settings' },
{ id: 'security', title: '安全设置', path: '/pages/mall/admin/system-settings?tab=security' },
{ id: 'email', title: '邮件设置', path: '/pages/mall/admin/system-settings?tab=email' }
]
}
]
}
])
/** ✅ 关键:把 currentPage可能是子菜单 id反推到顶级菜单 + 子菜单 */
const resolveActiveByPageId = (pageId: string) => {
// 1) 命中顶级
const top = menuList.value.find(m => m.id === pageId)
if (top) {
const first = getFirstChild(top)
return { menuId: top.id, subId: first ? first.id : '', groupId: first ? first.groupId : '' }
}
// 2) 命中二级groups.children
for (let i = 0; i < menuList.value.length; i++) {
const m = menuList.value[i]
const gs = m.groups || []
for (let g = 0; g < gs.length; g++) {
const group = gs[g]
for (let c = 0; c < group.children.length; c++) {
const child = group.children[c]
if (child.id === pageId) {
return { menuId: m.id, subId: child.id, groupId: group.id }
}
}
}
}
// 3) 找不到:回退到首页
return { menuId: 'home', subId: '', groupId: '' }
}
const getFirstChild = (menu: MenuItem) => {
const gs = menu.groups || []
if (gs.length === 0) return null
if (gs[0].children.length === 0) return null
return { ...gs[0].children[0], groupId: gs[0].id }
}
const setOpenGroupsForMenu = (menuId: string, forceOpenGroupId: string) => {
const m = menuList.value.find(x => x.id === menuId)
const gs = (m && m.groups) ? m.groups : []
// 默认全展开;如果传了 forceOpenGroupId确保它在列表里
const ids = gs.map(g => g.id)
openGroupIds.value = ids
if (forceOpenGroupId && ids.indexOf(forceOpenGroupId) < 0) {
openGroupIds.value.push(forceOpenGroupId)
}
}
watch(
() => props.currentPage,
(val) => {
const r = resolveActiveByPageId(val || 'home')
activeMenu.value = r.menuId
activeSubMenu.value = r.subId
setOpenGroupsForMenu(r.menuId, r.groupId)
},
{ immediate: true }
)
const activeMenuTitle = computed(() => {
const m = menuList.value.find(x => x.id === activeMenu.value)
return m ? m.title : '后台'
})
const activeGroups = computed(() => {
const m = menuList.value.find(x => x.id === activeMenu.value)
return (m && m.groups) ? m.groups : []
})
const activeSubMenuTitle = computed(() => {
const gs = activeGroups.value
for (let g = 0; g < gs.length; g++) {
const group = gs[g]
const sub = group.children.find(x => x.id === activeSubMenu.value)
if (sub) return sub.title
}
return ''
})
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const isGroupOpen = (groupId: string) => {
return openGroupIds.value.indexOf(groupId) >= 0
}
const toggleGroup = (groupId: string) => {
const idx = openGroupIds.value.indexOf(groupId)
if (idx >= 0) openGroupIds.value.splice(idx, 1)
else openGroupIds.value.push(groupId)
}
// ✅ 后台切页建议用 redirectTo避免页面栈越堆越深目标页必须在 pages.json 注册 :contentReference[oaicite:2]{index=2}
const go = (url: string) => {
uni.redirectTo({ url })
}
const handleMenuClick = (menu: MenuItem) => {
activeMenu.value = menu.id
const first = getFirstChild(menu)
if (first) {
activeSubMenu.value = first.id
setOpenGroupsForMenu(menu.id, first.groupId || '')
go(first.path)
} else {
activeSubMenu.value = ''
setOpenGroupsForMenu(menu.id, '')
go(menu.path)
}
}
const handleSubMenuClick = (subMenu: SubMenu, groupId: string) => {
activeSubMenu.value = subMenu.id
if (!isGroupOpen(groupId)) openGroupIds.value.push(groupId)
go(subMenu.path)
}
const handleSearch = () => {
uni.showToast({ title: '搜索功能', icon: 'none' })
}
const handleNotification = () => {
uni.showToast({ title: '通知中心', icon: 'none' })
}
const handleFullscreen = () => {
uni.showToast({ title: '全屏切换', icon: 'none' })
}
const handleUserMenu = () => {
uni.showActionSheet({
itemList: ['个人信息', '退出登录'],
success: (res) => {
uni.showToast({ title: res.tapIndex === 0 ? '个人信息' : '退出登录', icon: 'none' })
}
})
}
</script>
<style>
.admin-layout {
display: flex;
height: 100vh;
background-color: #f0f2f5;
}
/* ===== 主侧边栏 ===== */
.admin-sider {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 220px;
background-color: #001529;
z-index: 1000;
display: flex;
flex-direction: column;
transition: width 0.25s ease;
}
.admin-sider.sider-collapsed {
width: 80px;
}
.sider-header {
height: 64px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #002140;
border-bottom: 1px solid #002140;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo-img {
width: 28px;
height: 28px;
}
.logo-text {
color: #fff;
font-size: 16px;
font-weight: 600;
}
.collapse-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
border-radius: 6px;
}
.collapse-btn:active {
background-color: rgba(255,255,255,0.12);
}
.menu-primary {
flex: 1;
padding: 10px 0;
}
.menu-item-primary {
margin: 8px 10px;
padding: 10px 8px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: rgba(255,255,255,0.80);
}
.menu-item-primary.active {
background-color: #1890ff;
color: #fff;
}
.menu-item-primary:active {
background-color: rgba(24,144,255,0.65);
}
.menu-icon-img {
width: 22px;
height: 22px;
}
.menu-title {
font-size: 12px;
color: inherit;
}
/* ===== 主内容区:二级侧边栏 + 内容区(横向布局,避免二级菜单跑到顶部)===== */
.admin-main {
margin-left: 220px;
width: calc(100% - 220px);
height: 100vh;
display: flex; /* 关键:横向 */
flex-direction: row; /* 关键:横向 */
transition: margin-left 0.25s ease, width 0.25s ease;
}
.admin-main.main-collapsed {
margin-left: 80px;
width: calc(100% - 80px);
}
/* ===== 二级侧边栏(分组导航)===== */
.content-sider {
width: 220px;
height: 100%;
background-color: #ffffff;
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
}
.content-sider-header {
height: 48px;
padding: 0 16px;
display: flex;
align-items: center;
border-bottom: 1px solid #f0f0f0;
background-color: #fafafa;
}
.content-sider-title {
font-size: 14px;
font-weight: 600;
color: #262626;
}
.content-sider-scroll {
flex: 1;
}
.group-wrap {
padding: 10px 0;
}
.group-block {
margin-bottom: 10px;
}
.group-header {
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: space-between;
color: #595959;
}
.group-title {
font-size: 13px;
font-weight: 600;
}
.group-arrow {
font-size: 12px;
color: #8c8c8c;
}
.group-children {
padding: 4px 0;
}
.content-sub-menu-item {
height: 38px;
padding: 0 16px;
display: flex;
align-items: center;
color: #595959;
border-left: 3px solid transparent;
}
.content-sub-menu-item:active {
background-color: #f5f5f5;
}
.content-sub-menu-item.active {
background-color: #e6f7ff;
color: #1890ff;
border-left-color: #1890ff;
font-weight: 600;
}
.content-sub-menu-text {
font-size: 13px;
}
/* ===== 右侧内容区 ===== */
.content-area {
flex: 1;
min-width: 0; /* 防止横向溢出 */
height: 100%;
display: flex;
flex-direction: column;
background-color: #f0f2f5;
}
/* 顶部 Header */
.admin-header {
height: 64px;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
border-bottom: 1px solid #e8e8e8;
}
.breadcrumb {
font-size: 15px;
color: #666666;
}
.breadcrumb-item {
color: #1890ff;
}
.breadcrumb-item.active {
color: #262626;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header-action {
width: 38px;
height: 38px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.header-action:active {
background-color: #f5f5f5;
}
.notification-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background-color: #ff4d4f;
border-radius: 50%;
border: 2px solid #fff;
}
.header-user {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 10px;
}
.header-user:active {
background-color: #f5f5f5;
}
.user-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
}
.user-name {
font-size: 13px;
color: #666;
}
/* 页面内容区 */
.page-content {
flex: 1;
height: 100%;
background-color: #f0f2f5;
}
.page-wrapper {
padding: 18px;
min-height: 100%;
}
/* iconfont保留你现有的 header/折叠按钮图标) */
.iconfont {
font-family: 'iconfont';
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>