703 lines
15 KiB
Plaintext
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> |