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

703 lines
15 KiB
Plaintext

<template>
<view class="admin-layout">
<!-- 左侧固定深色 Sider -->
<view class="admin-sider" :class="{ 'sider-collapsed': isCollapsed }">
<!-- Logo区域 -->
<view class="sider-header">
<view class="logo">
<text class="logo-text">{{ isCollapsed ? 'M' : 'Mall Admin' }}</text>
</view>
<view class="collapse-btn" @click="toggleCollapse">
<text class="iconfont">{{ isCollapsed ? 'icon-menu-unfold' : 'icon-menu-fold' }}</text>
</view>
</view>
<!-- 一级菜单 (icon bar) -->
<view class="menu-primary">
<view
v-for="menu in menuList"
:key="menu.id"
class="menu-item-primary"
:class="{ 'active': activeMenu === menu.id }"
@click="handleMenuClick(menu)"
>
<text class="iconfont menu-icon">{{ menu.icon }}</text>
</view>
</view>
<!-- 二级菜单栏 (当前一级下的文字菜单) -->
<view class="menu-secondary" v-if="!isCollapsed">
<view class="secondary-header">
<text class="secondary-title">{{ activeMenuTitle }}</text>
</view>
<view class="secondary-content">
<view
v-for="subMenu in activeSubMenus"
:key="subMenu.id"
class="sub-menu-item"
:class="{ 'active': activeSubMenu === subMenu.id }"
@click="handleSubMenuClick(subMenu)"
>
<text class="sub-menu-text">{{ subMenu.title }}</text>
</view>
</view>
</view>
</view>
<!-- 主内容区 -->
<view class="admin-main" :class="{ 'main-collapsed': isCollapsed }">
<!-- 顶部 Header -->
<view class="admin-header">
<view class="header-left">
<view class="breadcrumb">
<text class="breadcrumb-item">{{ currentPageTitle }}</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" />
<text class="user-name">管理员</text>
<text class="iconfont icon-down"></text>
</view>
</view>
</view>
<!-- 多标签页 Tab Bar -->
<view class="admin-tabs">
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false">
<view class="tabs-container">
<view
v-for="tab in tabs"
:key="tab.id"
class="tab-item"
:class="{ 'active': activeTab === tab.id }"
@click="switchTab(tab.id)"
>
<text class="tab-title">{{ tab.title }}</text>
<view class="tab-close" v-if="tabs.length > 1" @click.stop="closeTab(tab.id)">
<text class="iconfont icon-close"></text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 页面内容区 (flex:1 height:0 scroll-view) -->
<scroll-view class="page-content" scroll-y="true" :style="{ flex: '1', height: '0' }">
<view class="page-wrapper">
<slot></slot>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, computed } from 'vue'
// Props
const props = defineProps<{
currentPage: string
}>()
// 响应式数据
const isCollapsed = ref(false)
const activeMenu = ref('dashboard')
const activeSubMenu = ref('')
const hasNotification = ref(true)
// 标签页数据
const tabs = ref([
{ id: 'dashboard', title: '首页', closable: false }
])
const activeTab = ref('dashboard')
// 菜单数据 (CRMEB 风格)
const menuList = ref([
{
id: 'dashboard',
title: '首页',
icon: 'icon-dashboard',
path: '/pages/mall/admin/index',
subMenus: []
},
{
id: 'user',
title: '用户管理',
icon: 'icon-user',
path: '/pages/mall/admin/user-management',
subMenus: [
{ 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: 'icon-shopping',
path: '/pages/mall/admin/product-management',
subMenus: [
{ 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: 'icon-order',
path: '/pages/mall/admin/order-management',
subMenus: [
{ 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: 'icon-finance',
path: '/pages/mall/admin/finance-management',
subMenus: [
{ 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: 'icon-statistics',
path: '/pages/mall/admin/user-statistics',
subMenus: [
{ id: 'user-stats', title: '用户统计', path: '/pages/mall/admin/user-statistics' },
{ id: 'behavior', title: '用户行为', path: '/pages/mall/admin/user-statistics?tab=behavior' }
]
},
{
id: 'system',
title: '系统设置',
icon: 'icon-setting',
path: '/pages/mall/admin/system-settings',
subMenus: [
{ 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' }
]
}
])
// 计算属性
const activeMenuTitle = computed(() => {
const menu = menuList.value.find(m => m.id === activeMenu.value)
return menu ? menu.title : '管理后台'
})
const activeSubMenus = computed(() => {
const menu = menuList.value.find(m => m.id === activeMenu.value)
return menu ? menu.subMenus || [] : []
})
const currentPageTitle = computed(() => {
const menu = menuList.value.find(m => m.id === props.currentPage)
return menu ? menu.title : '管理后台'
})
// 方法
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
const handleMenuClick = (menu: any) => {
activeMenu.value = menu.id
activeSubMenu.value = menu.subMenus && menu.subMenus.length > 0 ? menu.subMenus[0].id : ''
// 添加或切换标签页
const existingTab = tabs.value.find(tab => tab.id === menu.id)
if (!existingTab) {
tabs.value.push({
id: menu.id,
title: menu.title,
closable: true
})
}
activeTab.value = menu.id
// 只跳转到已在 pages.json 中声明的页面,不带 query 参数
uni.navigateTo({
url: menu.path
})
}
const handleSubMenuClick = (subMenu: any) => {
activeSubMenu.value = subMenu.id
// ✅ 保留完整的路径(包括 query 参数),让页面内部通过 onLoad(options) 处理 tab 切换
uni.navigateTo({
url: 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) => {
if (res.tapIndex === 0) {
uni.showToast({
title: '个人信息',
icon: 'none'
})
} else {
uni.showToast({
title: '退出登录',
icon: 'none'
})
}
}
})
}
const switchTab = (tabId: string) => {
activeTab.value = tabId
const menu = menuList.value.find(m => m.id === tabId)
if (menu) {
activeMenu.value = tabId
uni.navigateTo({
url: menu.path
})
}
}
const closeTab = (tabId: string) => {
const index = tabs.value.findIndex(tab => tab.id === tabId)
if (index > -1 && tabs.value[index].closable) {
tabs.value.splice(index, 1)
// 如果关闭的是当前标签,切换到上一个
if (activeTab.value === tabId && tabs.value.length > 0) {
activeTab.value = tabs.value[Math.max(0, index - 1)].id
const menu = menuList.value.find(m => m.id === activeTab.value)
if (menu) {
activeMenu.value = activeTab.value
uni.navigateTo({
url: menu.path
})
}
}
}
}
</script>
<style>
/* ===== AdminLayout 样式 ===== */
.admin-layout {
display: flex;
height: 100vh;
background-color: #f0f2f5;
}
/* ===== 左侧 Sider ===== */
.admin-sider {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 240px;
background-color: #001529;
transition: width 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.admin-sider.sider-collapsed {
width: 80px;
}
/* Sider Header */
.sider-header {
height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 1px solid #002140;
background-color: #002140;
}
.logo {
display: flex;
align-items: center;
}
.logo-text {
color: #ffffff;
font-size: 18px;
font-weight: 600;
}
.collapse-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #ffffff;
border-radius: 4px;
transition: background-color 0.2s;
}
.collapse-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 一级菜单 (Icon Bar) */
.menu-primary {
padding: 16px 0;
border-bottom: 1px solid #002140;
}
.menu-item-primary {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
margin: 8px 16px;
cursor: pointer;
color: rgba(255, 255, 255, 0.65);
border-radius: 6px;
transition: all 0.2s;
}
.menu-item-primary:hover {
background-color: #1890ff;
color: #ffffff;
}
.menu-item-primary.active {
background-color: #1890ff;
color: #ffffff;
}
.menu-icon {
font-size: 20px;
}
/* 二级菜单栏 */
.menu-secondary {
flex: 1;
display: flex;
flex-direction: column;
background-color: #000c17;
}
.secondary-header {
padding: 16px 24px;
border-bottom: 1px solid #002140;
}
.secondary-title {
color: #ffffff;
font-size: 14px;
font-weight: 500;
}
.secondary-content {
flex: 1;
padding: 8px 0;
}
.sub-menu-item {
display: flex;
align-items: center;
height: 40px;
padding: 0 24px;
cursor: pointer;
color: rgba(255, 255, 255, 0.65);
transition: all 0.2s;
}
.sub-menu-item:hover {
background-color: rgba(255, 255, 255, 0.05);
color: #ffffff;
}
.sub-menu-item.active {
background-color: rgba(24, 144, 255, 0.2);
color: #1890ff;
border-right: 3px solid #1890ff;
}
.sub-menu-text {
font-size: 14px;
}
/* ===== 主内容区 ===== */
.admin-main {
flex: 1;
margin-left: 240px;
transition: margin-left 0.3s ease;
display: flex;
flex-direction: column;
min-height: 100vh;
}
.admin-main.main-collapsed {
margin-left: 80px;
}
/* ===== 顶部 Header ===== */
.admin-header {
height: 64px;
background-color: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
border-bottom: 1px solid #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.header-left {
display: flex;
align-items: center;
}
.breadcrumb {
font-size: 16px;
color: #666666;
}
.breadcrumb-item {
color: #1890ff;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.header-action {
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #666666;
border-radius: 6px;
transition: all 0.2s;
}
.header-action:hover {
background-color: #f5f5f5;
color: #1890ff;
}
.notification-dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background-color: #ff4d4f;
border-radius: 50%;
border: 2px solid #ffffff;
}
.header-user {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 8px 12px;
border-radius: 6px;
transition: background-color 0.2s;
}
.header-user:hover {
background-color: #f5f5f5;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.user-name {
font-size: 14px;
color: #666666;
}
/* ===== 多标签页 Tab Bar ===== */
.admin-tabs {
height: 44px;
background-color: #ffffff;
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0;
}
.tabs-scroll {
height: 100%;
}
.tabs-container {
display: flex;
height: 100%;
min-width: 100%;
}
.tab-item {
display: flex;
align-items: center;
padding: 0 16px;
height: 100%;
cursor: pointer;
border-right: 1px solid #e8e8e8;
background-color: #fafafa;
transition: all 0.2s;
position: relative;
}
.tab-item:hover {
background-color: #f0f0f0;
}
.tab-item.active {
background-color: #ffffff;
border-bottom: 2px solid #1890ff;
color: #1890ff;
}
.tab-title {
font-size: 14px;
margin-right: 8px;
white-space: nowrap;
}
.tab-close {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s;
}
.tab-close:hover {
background-color: #e8e8e8;
}
/* ===== 页面内容区 ===== */
.page-content {
flex: 1;
background-color: #f0f2f5;
}
.page-wrapper {
padding: 24px;
min-height: calc(100vh - 108px);
}
/* ===== 响应式设计 ===== */
@media (max-width: 768px) {
.admin-sider {
width: 200px;
}
.admin-sider.sider-collapsed {
width: 0;
overflow: hidden;
}
.admin-main {
margin-left: 200px;
}
.admin-main.main-collapsed {
margin-left: 0;
}
.admin-header {
padding: 0 16px;
}
.page-wrapper {
padding: 16px;
}
.header-right {
gap: 8px;
}
.header-action {
width: 36px;
height: 36px;
}
}
/* ===== 图标字体 ===== */
.iconfont {
font-family: 'iconfont';
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ===== 滚动条样式 ===== */
.admin-sider ::-webkit-scrollbar,
.page-content ::-webkit-scrollbar {
width: 6px;
}
.admin-sider ::-webkit-scrollbar-track,
.page-content ::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.admin-sider ::-webkit-scrollbar-thumb,
.page-content ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.admin-sider ::-webkit-scrollbar-thumb:hover,
.page-content ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>