继续完善页面布局
This commit is contained in:
@@ -1,598 +0,0 @@
|
||||
<!-- CRMEB Admin Aside组件 - uni-app版本 -->
|
||||
<template>
|
||||
<!-- 桌面端侧边栏 -->
|
||||
<view
|
||||
class="admin-aside"
|
||||
:class="{ 'aside-collapsed': isCollapsed }"
|
||||
v-if="!isMobile()"
|
||||
>
|
||||
<!-- Logo区域 -->
|
||||
<view class="aside-header">
|
||||
<view class="logo-section">
|
||||
<view class="logo" v-if="!isCollapsed">
|
||||
<text class="logo-text">Mall Admin</text>
|
||||
</view>
|
||||
<view class="logo-mini" v-else>
|
||||
<text class="logo-text">MA</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="collapse-btn" @click="$emit('toggle-collapse')">
|
||||
<text class="iconfont" :class="isCollapsed ? 'icon-menu' : 'icon-close'"></text>
|
||||
</view>
|
||||
</aside-header>
|
||||
|
||||
<!-- 菜单区域 -->
|
||||
<scroll-view class="menu-scroll" scroll-y="true">
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="(menu, index) in menuList"
|
||||
:key="menu.id"
|
||||
class="menu-item"
|
||||
:class="{
|
||||
'menu-item-active': activeMenu === menu.id,
|
||||
'menu-item-open': menu.children && menu.children.length > 0 && isMenuOpen(menu.id)
|
||||
}"
|
||||
>
|
||||
<!-- 一级菜单 -->
|
||||
<view
|
||||
class="menu-link"
|
||||
:class="{ 'menu-link-active': activeMenu === menu.id }"
|
||||
@click="handleMenuClick(menu)"
|
||||
>
|
||||
<view class="menu-icon">
|
||||
<text class="iconfont" :class="menu.icon"></text>
|
||||
</view>
|
||||
<text class="menu-text" v-if="!isCollapsed">{{ menu.title }}</text>
|
||||
<view class="menu-arrow" v-if="menu.children && menu.children.length > 0 && !isCollapsed">
|
||||
<text class="iconfont icon-arrow-down"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<view
|
||||
class="submenu"
|
||||
v-if="menu.children && menu.children.length > 0 && (!isCollapsed || isMenuOpen(menu.id))"
|
||||
>
|
||||
<view
|
||||
v-for="(subMenu, subIndex) in menu.children"
|
||||
:key="subMenu.id"
|
||||
class="submenu-item"
|
||||
:class="{ 'submenu-item-active': activeMenu === subMenu.id }"
|
||||
@click="handleMenuClick(subMenu)"
|
||||
>
|
||||
<text class="submenu-text" v-if="!isCollapsed">{{ subMenu.title }}</text>
|
||||
<text class="submenu-text-collapsed" v-else>{{ subMenu.title.charAt(0) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部用户信息 -->
|
||||
<view class="aside-footer" v-if="!isCollapsed">
|
||||
<view class="user-info">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ userInfo.nickname ? userInfo.nickname.charAt(0) : 'A' }}</text>
|
||||
</view>
|
||||
<view class="user-details">
|
||||
<text class="user-name">{{ userInfo.nickname || '管理员' }}</text>
|
||||
<text class="user-role">{{ userInfo.role || 'admin' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 移动端抽屉 -->
|
||||
<view
|
||||
class="mobile-drawer"
|
||||
:class="{ 'drawer-open': !isCollapsed }"
|
||||
v-else
|
||||
>
|
||||
<view class="drawer-overlay" @click="$emit('toggle-collapse')"></view>
|
||||
<view class="drawer-content">
|
||||
<!-- 移动端头部 -->
|
||||
<view class="mobile-header">
|
||||
<text class="mobile-title">菜单</text>
|
||||
<view class="close-btn" @click="$emit('toggle-collapse')">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 移动端菜单 -->
|
||||
<scroll-view class="mobile-menu-scroll" scroll-y="true">
|
||||
<view class="mobile-menu-list">
|
||||
<view
|
||||
v-for="(menu, index) in menuList"
|
||||
:key="menu.id"
|
||||
class="mobile-menu-item"
|
||||
:class="{
|
||||
'menu-item-active': activeMenu === menu.id,
|
||||
'menu-item-open': menu.children && menu.children.length > 0 && isMenuOpen(menu.id)
|
||||
}"
|
||||
>
|
||||
<!-- 一级菜单 -->
|
||||
<view
|
||||
class="mobile-menu-link"
|
||||
:class="{ 'menu-link-active': activeMenu === menu.id }"
|
||||
@click="handleMenuClick(menu)"
|
||||
>
|
||||
<view class="menu-icon">
|
||||
<text class="iconfont" :class="menu.icon"></text>
|
||||
</view>
|
||||
<text class="menu-text">{{ menu.title }}</text>
|
||||
<view class="menu-arrow" v-if="menu.children && menu.children.length > 0">
|
||||
<text class="iconfont icon-arrow-down"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<view
|
||||
class="mobile-submenu"
|
||||
v-if="menu.children && menu.children.length > 0 && isMenuOpen(menu.id)"
|
||||
>
|
||||
<view
|
||||
v-for="(subMenu, subIndex) in menu.children"
|
||||
:key="subMenu.id"
|
||||
class="mobile-submenu-item"
|
||||
:class="{ 'submenu-item-active': activeMenu === subMenu.id }"
|
||||
@click="handleMenuClick(subMenu)"
|
||||
>
|
||||
<text class="submenu-text">{{ subMenu.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import type { MenuItem, UserInfo } from './types.uts'
|
||||
|
||||
export default {
|
||||
name: 'AdminAside',
|
||||
props: {
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
menuList: {
|
||||
type: Array as () => MenuItem[],
|
||||
default: () => []
|
||||
},
|
||||
activeMenu: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
openMenus: {
|
||||
type: Array as () => string[],
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
} as UserInfo
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
methods: {
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const userInfo = uni.getStorageSync('user_info')
|
||||
if (userInfo) {
|
||||
this.userInfo = userInfo
|
||||
}
|
||||
},
|
||||
|
||||
// 处理菜单点击
|
||||
handleMenuClick(menu: MenuItem) {
|
||||
this.$emit('menu-click', menu)
|
||||
},
|
||||
|
||||
// 检查菜单是否展开
|
||||
isMenuOpen(menuId: string): boolean {
|
||||
return this.openMenus.includes(menuId)
|
||||
},
|
||||
|
||||
// 判断是否为移动端
|
||||
isMobile(): boolean {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
return systemInfo.windowWidth < 768
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 桌面端侧边栏 */
|
||||
.admin-aside {
|
||||
width: 240rpx;
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transition: width 0.3s ease;
|
||||
z-index: 1000;
|
||||
|
||||
&.aside-collapsed {
|
||||
width: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 侧边栏头部 */
|
||||
.aside-header {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #1a1a1a;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 菜单滚动区域 */
|
||||
.menu-scroll {
|
||||
flex: 1;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
/* 菜单项样式 */
|
||||
.menu-item {
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.menu-item-active {
|
||||
.menu-link {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4rpx;
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-item-open {
|
||||
.submenu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.menu-link-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
margin-right: 20rpx;
|
||||
|
||||
.iconfont {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
transition: transform 0.3s;
|
||||
|
||||
.iconfont {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 子菜单样式 */
|
||||
.submenu {
|
||||
margin-top: 10rpx;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx 0 80rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 6rpx;
|
||||
transition: all 0.3s;
|
||||
font-size: 26rpx;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.submenu-item-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-text {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.submenu-text-collapsed {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
/* 侧边栏底部 */
|
||||
.aside-footer {
|
||||
padding: 20rpx;
|
||||
border-top: 1rpx solid #1a1a1a;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* 移动端抽屉 */
|
||||
.mobile-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.drawer-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
position: relative;
|
||||
width: 280rpx;
|
||||
height: 100%;
|
||||
background-color: #001529;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
/* 移动端头部 */
|
||||
.mobile-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #1a1a1a;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端菜单 */
|
||||
.mobile-menu-scroll {
|
||||
flex: 1;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.mobile-menu-list {
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.mobile-menu-item {
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
&.menu-item-active {
|
||||
.mobile-menu-link {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-item-open {
|
||||
.mobile-submenu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.menu-link-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-submenu {
|
||||
margin-top: 10rpx;
|
||||
display: none;
|
||||
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx 0 80rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 6rpx;
|
||||
transition: all 0.3s;
|
||||
font-size: 26rpx;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.submenu-item-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,281 +0,0 @@
|
||||
<!-- CRMEB Admin Breadcrumb组件 - uni-app版本 -->
|
||||
<template>
|
||||
<view class="admin-breadcrumb">
|
||||
<!-- 左侧:菜单按钮 + 面包屑 -->
|
||||
<view class="breadcrumb-left">
|
||||
<!-- 菜单折叠按钮 -->
|
||||
<view class="menu-toggle" @click="$emit('toggle-collapse')">
|
||||
<text class="iconfont icon-menu"></text>
|
||||
</view>
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<view class="breadcrumb-content">
|
||||
<text class="breadcrumb-home" @click="goHome">首页</text>
|
||||
<text class="breadcrumb-separator">/</text>
|
||||
<text class="breadcrumb-current">{{ currentPageTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧:用户信息和操作 -->
|
||||
<view class="breadcrumb-right">
|
||||
<!-- 通知中心 -->
|
||||
<view class="nav-item" @click="handleNotification">
|
||||
<text class="iconfont icon-notification"></text>
|
||||
<view class="badge" v-if="notificationCount > 0">{{ notificationCount }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-section" @click="handleProfile">
|
||||
<view class="user-avatar">
|
||||
<text class="avatar-text">{{ userInfo.nickname ? userInfo.nickname.charAt(0) : 'A' }}</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{ userInfo.nickname || '管理员' }}</text>
|
||||
<text class="user-role">{{ userInfo.role || 'admin' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import type { UserInfo } from './types.uts'
|
||||
|
||||
export default {
|
||||
name: 'AdminBreadcrumb',
|
||||
props: {
|
||||
currentPageTitle: {
|
||||
type: String,
|
||||
default: '管理后台'
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userInfo: {
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
} as UserInfo,
|
||||
notificationCount: 3
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
methods: {
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const userInfo = uni.getStorageSync('user_info')
|
||||
if (userInfo) {
|
||||
this.userInfo = userInfo
|
||||
}
|
||||
},
|
||||
|
||||
// 返回首页
|
||||
goHome() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/index'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理通知
|
||||
handleNotification() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/mall/admin/notifications'
|
||||
})
|
||||
},
|
||||
|
||||
// 处理个人资料
|
||||
handleProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/user/profile'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 左侧区域 */
|
||||
.breadcrumb-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 菜单切换按钮 */
|
||||
.menu-toggle {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
margin-right: 20rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 面包屑内容 */
|
||||
.breadcrumb-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb-home {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
margin: 0 10rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
.breadcrumb-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 导航项 */
|
||||
.nav-item {
|
||||
position: relative;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
border-radius: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 消息徽章 */
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
background-color: #ff4d4f;
|
||||
color: #fff;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
padding: 0 6rpx;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 用户信息区域 */
|
||||
.user-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-left: 20rpx;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768rpx) {
|
||||
.admin-breadcrumb {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.breadcrumb-content {
|
||||
display: none; /* 移动端隐藏面包屑 */
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: none; /* 移动端隐藏用户信息文字 */
|
||||
}
|
||||
|
||||
.user-section {
|
||||
padding: 12rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
layouts/admin/components/AdminAside.uvue
Normal file
81
layouts/admin/components/AdminAside.uvue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view class="aside">
|
||||
<view class="aside-header">
|
||||
<view class="logo">
|
||||
<text class="logo-text">{{ collapsed ? '商' : '商城后台' }}</text>
|
||||
</view>
|
||||
<view class="collapse-btn" @click="$emit('toggle')">
|
||||
<text class="collapse-text">{{ collapsed ? '›' : '‹' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="aside-menu">
|
||||
<view
|
||||
v-for="m in menuList"
|
||||
:key="m.id"
|
||||
class="aside-item"
|
||||
:class="{ active: activeMenuId === m.id }"
|
||||
@click="$emit('menu-click', m.id)"
|
||||
>
|
||||
<image class="aside-icon" :src="m.icon" mode="aspectFit" />
|
||||
<text class="aside-title" v-if="!collapsed">{{ m.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import type { MenuItem } from '../types.uts'
|
||||
|
||||
defineProps<{
|
||||
collapsed: boolean
|
||||
menuList: MenuItem[]
|
||||
activeMenuId: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e:'toggle'): void
|
||||
(e:'menu-click', menuId: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.aside{
|
||||
width: 96px;
|
||||
background: #1f2a37;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
}
|
||||
.aside-header{
|
||||
height: 56px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.logo-text{ color:#fff; font-size:14px; font-weight:600; }
|
||||
.collapse-btn{ width:28px; height:28px; display:flex; align-items:center; justify-content:center; }
|
||||
.collapse-text{ color:rgba(255,255,255,0.7); }
|
||||
|
||||
.aside-menu{ padding: 8px 0; }
|
||||
.aside-item{
|
||||
height: 54px;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap: 6px;
|
||||
margin: 6px 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.aside-item.active{ background:#1677ff; }
|
||||
.aside-icon{ width:22px; height:22px; }
|
||||
.aside-title{ color:#fff; font-size:12px; }
|
||||
</style>
|
||||
20
layouts/admin/components/AdminFooter.uvue
Normal file
20
layouts/admin/components/AdminFooter.uvue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<view class="footer">
|
||||
<text class="footer-text">商城后台 © {{ year }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const year = new Date().getFullYear()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.footer{
|
||||
height: 44px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.footer-text{ font-size:12px; }
|
||||
</style>
|
||||
63
layouts/admin/components/AdminHeader.uvue
Normal file
63
layouts/admin/components/AdminHeader.uvue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="header">
|
||||
<view class="header-left">
|
||||
<text class="crumb">{{ breadcrumb }}</text>
|
||||
</view>
|
||||
|
||||
<view class="header-right">
|
||||
<view class="hbtn" @click="$emit('search')"><text>🔍</text></view>
|
||||
<view class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
|
||||
<view class="hbtn" @click="$emit('notify')">
|
||||
<text>🔔</text>
|
||||
<view class="dot" v-if="hasNotification"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
defineProps<{
|
||||
breadcrumb: string
|
||||
hasNotification: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e:'search'): void
|
||||
(e:'refresh'): void
|
||||
(e:'notify'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header{
|
||||
height: 56px;
|
||||
background:#fff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.crumb{ color:#374151; font-size:14px; }
|
||||
|
||||
.header-right{ display:flex; align-items:center; gap: 10px; }
|
||||
.hbtn{
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background:#f6f7fb;
|
||||
position: relative;
|
||||
}
|
||||
.dot{
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background:#ff4d4f;
|
||||
position:absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
</style>
|
||||
123
layouts/admin/components/AdminSubsider.uvue
Normal file
123
layouts/admin/components/AdminSubsider.uvue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view class="sub-sider" v-if="groups && groups.length > 0">
|
||||
<view class="sub-header">
|
||||
<text class="sub-title">{{ activeMenuTitle }}</text>
|
||||
<view class="sub-collapse" @click="collapsed = !collapsed">
|
||||
<text class="sub-collapse-text">{{ collapsed ? '›' : '‹' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="sub-body" scroll-y="true">
|
||||
<view v-for="(g, gi) in groups" :key="gi" class="group">
|
||||
<view class="group-title" @click="toggleGroup(g.title)">
|
||||
<text class="group-title-text">{{ g.title }}</text>
|
||||
<text class="group-arrow">{{ isGroupOpen(g.title) ? '˄' : '˅' }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="isGroupOpen(g.title)">
|
||||
<view
|
||||
v-for="c in g.children"
|
||||
:key="c.id"
|
||||
class="sub-item"
|
||||
:class="{ active: activeSubId === c.id }"
|
||||
@click="$emit('sub-click', c)"
|
||||
>
|
||||
<text class="sub-item-text" :class="{ activeText: activeSubId === c.id }">
|
||||
{{ c.title }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import type { MenuGroup, MenuChild } from '../types.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
activeMenuTitle: string
|
||||
groups: MenuGroup[]
|
||||
activeSubId: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e:'sub-click', child: MenuChild): void
|
||||
}>()
|
||||
|
||||
const collapsed = ref(false)
|
||||
const openGroups = ref<string[]>([])
|
||||
|
||||
const isGroupOpen = (title: string): boolean => {
|
||||
// 默认展开第一个分组 + 当前高亮分组(简单策略)
|
||||
if (openGroups.value.length === 0 && props.groups && props.groups.length > 0) return props.groups[0].title === title
|
||||
return openGroups.value.includes(title)
|
||||
}
|
||||
|
||||
const toggleGroup = (title: string) => {
|
||||
if (openGroups.value.includes(title)) {
|
||||
openGroups.value = openGroups.value.filter(t => t !== title)
|
||||
} else {
|
||||
openGroups.value = [...openGroups.value, title]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.sub-sider{
|
||||
width: 240px;
|
||||
background:#ffffff;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 96px; /* 紧贴主侧边栏 */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 900;
|
||||
}
|
||||
.sub-header{
|
||||
height: 56px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
.sub-title{ font-size:16px; font-weight:600; color:#111827; }
|
||||
.sub-collapse{ width:28px; height:28px; display:flex; align-items:center; justify-content:center; }
|
||||
.sub-collapse-text{ color:#6b7280; }
|
||||
|
||||
.sub-body{ height: calc(100vh - 56px); }
|
||||
|
||||
.group{ padding: 8px 0; }
|
||||
.group-title{
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content: space-between;
|
||||
color:#111827;
|
||||
}
|
||||
.group-title-text{ font-size:15px; font-weight:600; }
|
||||
.group-arrow{ color:#6b7280; }
|
||||
|
||||
.sub-item{
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
background: transparent;
|
||||
}
|
||||
.sub-item.active{
|
||||
background: #eaf2ff;
|
||||
}
|
||||
.sub-item-text{
|
||||
font-size:14px;
|
||||
color:#111827;
|
||||
}
|
||||
.sub-item-text.activeText{
|
||||
color:#1677ff;
|
||||
font-weight:600;
|
||||
}
|
||||
</style>
|
||||
76
layouts/admin/components/AdminTagsView.uvue
Normal file
76
layouts/admin/components/AdminTagsView.uvue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view class="tags">
|
||||
<scroll-view class="tags-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="tags-row">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.id"
|
||||
class="tag"
|
||||
:class="{ active: activeTabId === t.id }"
|
||||
@click="$emit('tab-click', t)"
|
||||
>
|
||||
<text class="tag-text">{{ t.title }}</text>
|
||||
<view class="tag-close" @click.stop="$emit('tab-close', t.id)">
|
||||
<text class="tag-close-text">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import type { TabItem } from '../types.uts'
|
||||
|
||||
defineProps<{
|
||||
tabs: TabItem[]
|
||||
activeTabId: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e:'tab-click', tab: TabItem): void
|
||||
(e:'tab-close', tabId: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tags{
|
||||
height: 44px;
|
||||
background:#fff;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
.tags-scroll{ width: 100%; height: 44px; }
|
||||
.tags-row{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
height: 44px;
|
||||
}
|
||||
.tag{
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background:#fff;
|
||||
border-radius: 6px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap: 8px;
|
||||
}
|
||||
.tag.active{
|
||||
border-color:#1677ff;
|
||||
background:#eaf2ff;
|
||||
}
|
||||
.tag-text{ font-size:12px; color:#374151; }
|
||||
.tag-close{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
.tag-close-text{ font-size:14px; color:#6b7280; line-height:14px; }
|
||||
</style>
|
||||
@@ -1,453 +0,0 @@
|
||||
<!-- CRMEB Admin Defaults布局 - uni-app版本 -->
|
||||
<template>
|
||||
<view class="layout-container">
|
||||
<!-- 侧边栏 -->
|
||||
<AdminAside
|
||||
:is-collapsed="isCollapsed"
|
||||
:menu-list="menuList"
|
||||
:active-menu="activeMenu"
|
||||
@toggle-collapse="handleToggleCollapse"
|
||||
@menu-click="handleMenuClick"
|
||||
/>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<view class="main-container" :class="{ 'main-collapsed': isCollapsed }">
|
||||
<!-- 顶部导航栏 -->
|
||||
<AdminHeader
|
||||
:is-collapsed="isCollapsed"
|
||||
:current-page-title="currentPageTitle"
|
||||
@toggle-collapse="handleToggleCollapse"
|
||||
/>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<view class="page-content">
|
||||
<scroll-view
|
||||
class="content-scroll"
|
||||
scroll-y="true"
|
||||
:style="{ height: contentHeight }"
|
||||
>
|
||||
<slot></slot>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 返回顶部按钮 -->
|
||||
<view
|
||||
class="back-top"
|
||||
v-if="showBackTop"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<text class="iconfont icon-top"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 移动端遮罩 -->
|
||||
<view
|
||||
class="mobile-overlay"
|
||||
v-if="showOverlay"
|
||||
@click="closeSidebar"
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminAside from './aside.uvue'
|
||||
import AdminHeader from './header.uvue'
|
||||
|
||||
import type { MenuItem } from './types.uts'
|
||||
|
||||
export default {
|
||||
name: 'AdminDefaults',
|
||||
components: {
|
||||
AdminAside,
|
||||
AdminHeader
|
||||
},
|
||||
props: {
|
||||
currentPage: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: false,
|
||||
showOverlay: false,
|
||||
showBackTop: false,
|
||||
activeMenu: '',
|
||||
openMenus: [] as string[],
|
||||
menuList: [] as MenuItem[],
|
||||
contentHeight: '100vh',
|
||||
scrollTop: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPageTitle() {
|
||||
const findMenuTitle = (menus: MenuItem[]): string => {
|
||||
for (const menu of menus) {
|
||||
if (menu.id === this.activeMenu) {
|
||||
return menu.title
|
||||
}
|
||||
if (menu.children) {
|
||||
const subTitle = findMenuTitle(menu.children)
|
||||
if (subTitle) return subTitle
|
||||
}
|
||||
}
|
||||
return '管理后台'
|
||||
}
|
||||
return findMenuTitle(this.menuList)
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.initLayout()
|
||||
this.initMenuData()
|
||||
this.updateActiveMenu()
|
||||
},
|
||||
onShow() {
|
||||
this.updateActiveMenu()
|
||||
},
|
||||
methods: {
|
||||
// 初始化布局
|
||||
initLayout() {
|
||||
// 获取侧边栏状态
|
||||
const collapsed = uni.getStorageSync('admin_sidebar_collapsed')
|
||||
this.isCollapsed = collapsed === true
|
||||
|
||||
// 获取展开的菜单
|
||||
const openMenus = uni.getStorageSync('admin_open_menus')
|
||||
if (openMenus) {
|
||||
this.openMenus = openMenus
|
||||
}
|
||||
|
||||
// 设置内容高度
|
||||
this.setContentHeight()
|
||||
},
|
||||
|
||||
// 初始化菜单数据
|
||||
initMenuData() {
|
||||
// 这里应该从配置或API获取菜单数据
|
||||
// 暂时使用静态数据,实际项目中应该从配置文件或API获取
|
||||
this.menuList = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: '首页',
|
||||
icon: 'icon-shujutongji',
|
||||
path: '/pages/mall/admin/index'
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: '用户管理',
|
||||
icon: 'icon-yonghuguanli',
|
||||
children: [
|
||||
{
|
||||
id: 'user-list',
|
||||
title: '用户列表',
|
||||
icon: 'icon-yonghuguanli',
|
||||
path: '/pages/mall/admin/user-management'
|
||||
},
|
||||
{
|
||||
id: 'user-detail',
|
||||
title: '用户详情',
|
||||
icon: 'icon-yonghuguanli',
|
||||
path: '/pages/mall/admin/user-detail'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'merchant',
|
||||
title: '商家管理',
|
||||
icon: 'icon-shangjiaguanli',
|
||||
children: [
|
||||
{
|
||||
id: 'merchant-list',
|
||||
title: '商家列表',
|
||||
icon: 'icon-shangjiaguanli',
|
||||
path: '/pages/mall/admin/merchant-management'
|
||||
},
|
||||
{
|
||||
id: 'merchant-review',
|
||||
title: '商家审核',
|
||||
icon: 'icon-shenhe',
|
||||
path: '/pages/mall/admin/merchant-review'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
title: '商品管理',
|
||||
icon: 'icon-shangpinguanli',
|
||||
children: [
|
||||
{
|
||||
id: 'product-list',
|
||||
title: '商品列表',
|
||||
icon: 'icon-shangpinguanli',
|
||||
path: '/pages/mall/admin/product-management'
|
||||
},
|
||||
{
|
||||
id: 'product-review',
|
||||
title: '商品审核',
|
||||
icon: 'icon-shenhe',
|
||||
path: '/pages/mall/admin/product-review'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: '订单管理',
|
||||
icon: 'icon-dingdanguanli',
|
||||
path: '/pages/mall/admin/order-management'
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
title: '财务管理',
|
||||
icon: 'icon-caiwuguanli',
|
||||
path: '/pages/mall/admin/finance-management'
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
title: '营销管理',
|
||||
icon: 'icon-yingxiaoguanli',
|
||||
children: [
|
||||
{
|
||||
id: 'coupon',
|
||||
title: '优惠券',
|
||||
icon: 'icon-youhuiquan',
|
||||
path: '/pages/mall/admin/coupon-management'
|
||||
},
|
||||
{
|
||||
id: 'marketing-main',
|
||||
title: '营销活动',
|
||||
icon: 'icon-yingxiaoguanli',
|
||||
path: '/pages/mall/admin/marketing-management'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'delivery',
|
||||
title: '配送管理',
|
||||
icon: 'icon-dingdanguanli',
|
||||
path: '/pages/mall/admin/delivery-management'
|
||||
},
|
||||
{
|
||||
id: 'subscription',
|
||||
title: '订阅管理',
|
||||
icon: 'icon-gongnengdaohang',
|
||||
children: [
|
||||
{
|
||||
id: 'plan-management',
|
||||
title: '方案管理',
|
||||
icon: 'icon-gongnengdaohang',
|
||||
path: '/pages/mall/admin/subscription/plan-management'
|
||||
},
|
||||
{
|
||||
id: 'user-subscriptions',
|
||||
title: '用户订阅',
|
||||
icon: 'icon-gongnengdaohang',
|
||||
path: '/pages/mall/admin/subscription/user-subscriptions'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: '审核管理',
|
||||
icon: 'icon-shenhe',
|
||||
children: [
|
||||
{
|
||||
id: 'product-review',
|
||||
title: '商品审核',
|
||||
icon: 'icon-shenhe',
|
||||
path: '/pages/mall/admin/product-review'
|
||||
},
|
||||
{
|
||||
id: 'merchant-review',
|
||||
title: '商家审核',
|
||||
icon: 'icon-shenhe',
|
||||
path: '/pages/mall/admin/merchant-review'
|
||||
},
|
||||
{
|
||||
id: 'refund-review',
|
||||
title: '退款处理',
|
||||
icon: 'icon-shenhe',
|
||||
path: '/pages/mall/admin/refund-review'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'complaints',
|
||||
title: '投诉处理',
|
||||
icon: 'icon-tousu',
|
||||
path: '/pages/mall/admin/complaints'
|
||||
},
|
||||
{
|
||||
id: 'notification',
|
||||
title: '通知中心',
|
||||
icon: 'icon-notification',
|
||||
path: '/pages/mall/admin/notifications'
|
||||
},
|
||||
{
|
||||
id: 'activity-log',
|
||||
title: '活动日志',
|
||||
icon: 'icon-rizhi',
|
||||
path: '/pages/mall/admin/activity-log'
|
||||
},
|
||||
{
|
||||
id: 'layout-test',
|
||||
title: '布局测试',
|
||||
icon: 'icon-kaifa',
|
||||
path: '/pages/mall/admin/layout-test'
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '系统设置',
|
||||
icon: 'icon-xitongshezhi',
|
||||
path: '/pages/mall/admin/system-settings'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 更新当前激活的菜单
|
||||
updateActiveMenu() {
|
||||
// 使用传入的currentPage prop来设置激活菜单
|
||||
if (this.currentPage) {
|
||||
this.activeMenu = this.currentPage
|
||||
}
|
||||
},
|
||||
|
||||
// 处理侧边栏切换
|
||||
handleToggleCollapse() {
|
||||
this.isCollapsed = !this.isCollapsed
|
||||
uni.setStorageSync('admin_sidebar_collapsed', this.isCollapsed)
|
||||
|
||||
// 移动端处理
|
||||
if (this.isMobile() && !this.isCollapsed) {
|
||||
this.showOverlay = true
|
||||
} else {
|
||||
this.showOverlay = false
|
||||
}
|
||||
},
|
||||
|
||||
// 处理菜单点击
|
||||
handleMenuClick(menu: MenuItem) {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
// 有子菜单,切换展开状态
|
||||
const index = this.openMenus.indexOf(menu.id)
|
||||
if (index > -1) {
|
||||
this.openMenus.splice(index, 1)
|
||||
} else {
|
||||
this.openMenus.push(menu.id)
|
||||
}
|
||||
uni.setStorageSync('admin_open_menus', this.openMenus)
|
||||
} else if (menu.path) {
|
||||
// 叶子节点,跳转页面
|
||||
this.activeMenu = menu.id
|
||||
uni.navigateTo({
|
||||
url: menu.path,
|
||||
fail: () => {
|
||||
// 如果navigateTo失败,尝试switchTab
|
||||
uni.switchTab({
|
||||
url: menu.path
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭侧边栏(移动端)
|
||||
closeSidebar() {
|
||||
if (this.isMobile()) {
|
||||
this.isCollapsed = true
|
||||
this.showOverlay = false
|
||||
uni.setStorageSync('admin_sidebar_collapsed', true)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置内容高度
|
||||
setContentHeight() {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
// 减去顶部导航栏高度(约88rpx)和底部安全区域
|
||||
this.contentHeight = `calc(${systemInfo.windowHeight}px - 88rpx)`
|
||||
},
|
||||
|
||||
// 滚动到顶部
|
||||
scrollToTop() {
|
||||
this.scrollTop = 0
|
||||
},
|
||||
|
||||
// 判断是否为移动端
|
||||
isMobile(): boolean {
|
||||
const systemInfo = uni.getSystemInfoSync()
|
||||
return systemInfo.windowWidth < 768
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 主内容区域 */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin-left 0.3s ease;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.main-collapsed {
|
||||
margin-left: 80rpx;
|
||||
}
|
||||
|
||||
/* 页面内容 */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: 100%;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
/* 返回顶部按钮 */
|
||||
.back-top {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #1890ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-top:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 移动端遮罩 */
|
||||
.mobile-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768rpx) {
|
||||
.main-collapsed {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,61 +0,0 @@
|
||||
<!-- CRMEB Admin Header组件 - uni-app版本 -->
|
||||
<template>
|
||||
<view class="admin-header">
|
||||
<!-- 导航栏容器 -->
|
||||
<view class="navbars-container">
|
||||
<!-- 面包屑导航 -->
|
||||
<AdminBreadcrumb
|
||||
:current-page-title="currentPageTitle"
|
||||
:is-collapsed="isCollapsed"
|
||||
@toggle-collapse="$emit('toggle-collapse')"
|
||||
/>
|
||||
|
||||
<!-- 标签页导航(如果启用) -->
|
||||
<AdminTagsView v-if="showTagsView" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminBreadcrumb from './breadcrumb.uvue'
|
||||
import AdminTagsView from './tags-view.uvue'
|
||||
|
||||
export default {
|
||||
name: 'AdminHeader',
|
||||
components: {
|
||||
AdminBreadcrumb,
|
||||
AdminTagsView
|
||||
},
|
||||
props: {
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentPageTitle: {
|
||||
type: String,
|
||||
default: '管理后台'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTagsView: false // 是否显示标签页,暂时关闭
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-header {
|
||||
height: 88rpx;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbars-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,351 +0,0 @@
|
||||
<template>
|
||||
<view class="admin-layout">
|
||||
<!-- 左侧侧边栏 -->
|
||||
<view class="admin-sider" :class="{ 'sider-collapsed': isCollapsed }">
|
||||
<view class="sider-header">
|
||||
<view class="logo">
|
||||
<text class="logo-text">{{ isCollapsed ? 'M' : 'Mall Admin' }}</text>
|
||||
</view>
|
||||
<view class="collapse-trigger" @click="toggleCollapse">
|
||||
<text class="iconfont">{{ isCollapsed ? 'icon-menu-unfold' : 'icon-menu-fold' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一级菜单 -->
|
||||
<scroll-view class="menu-scroll" scroll-y="true">
|
||||
<view class="menu-list">
|
||||
<view
|
||||
v-for="menu in menuList"
|
||||
:key="menu.id"
|
||||
class="menu-item"
|
||||
:class="{ 'menu-active': activeMenu === menu.id }"
|
||||
@click="handleMenuClick(menu)"
|
||||
>
|
||||
<view class="menu-icon">
|
||||
<text class="iconfont">{{ menu.icon }}</text>
|
||||
</view>
|
||||
<view class="menu-content" v-if="!isCollapsed">
|
||||
<text class="menu-text">{{ menu.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="admin-main" :class="{ 'main-collapsed': isCollapsed }">
|
||||
<!-- 顶部头部 -->
|
||||
<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-user">
|
||||
<text class="user-name">管理员</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<scroll-view class="page-content" scroll-y="true">
|
||||
<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 menuList = ref([
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: '首页',
|
||||
icon: 'icon-dashboard',
|
||||
path: '/pages/mall/admin/index'
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: '用户管理',
|
||||
icon: 'icon-user',
|
||||
path: '/pages/mall/admin/user-management'
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
title: '商品管理',
|
||||
icon: 'icon-shopping',
|
||||
path: '/pages/mall/admin/product-management'
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: '订单管理',
|
||||
icon: 'icon-order',
|
||||
path: '/pages/mall/admin/order-management'
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
title: '财务管理',
|
||||
icon: 'icon-finance',
|
||||
path: '/pages/mall/admin/finance-management'
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '系统设置',
|
||||
icon: 'icon-setting',
|
||||
path: '/pages/mall/admin/system-settings'
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
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
|
||||
uni.navigateTo({
|
||||
url: menu.path
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
uni.showToast({
|
||||
title: '搜索功能',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
.admin-sider {
|
||||
width: 240rpx;
|
||||
background: #001529;
|
||||
transition: width 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.sider-collapsed {
|
||||
width: 80rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.sider-header {
|
||||
height: 120rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
border-bottom: 1rpx solid #002140;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.collapse-trigger {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
border-radius: 6rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.menu-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
// 主内容区
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin-left 0.3s;
|
||||
}
|
||||
|
||||
// 头部
|
||||
.admin-header {
|
||||
height: 120rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
border-bottom: 1rpx solid #e8e8e8;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
border-radius: 6rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
cursor: pointer;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 6rpx;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// 页面内容
|
||||
.page-content {
|
||||
flex: 1;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 32rpx;
|
||||
min-height: calc(100vh - 120rpx);
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media screen and (max-width: 768rpx) {
|
||||
.admin-sider {
|
||||
width: 200rpx;
|
||||
|
||||
&.sider-collapsed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: 'iconfont';
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,641 +1,190 @@
|
||||
<template>
|
||||
<view class="admin-layout">
|
||||
<view class="layout-root">
|
||||
<!-- 主侧边栏 -->
|
||||
<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>
|
||||
<AdminAside
|
||||
:collapsed="isCollapsed"
|
||||
:menuList="menuList"
|
||||
:activeMenuId="activeMenuId"
|
||||
@toggle="toggleCollapse"
|
||||
@menu-click="onMenuClick"
|
||||
/>
|
||||
|
||||
<!-- 一级菜单(图标 + 文案;折叠时只显示图标) -->
|
||||
<scroll-view class="menu-primary" scroll-y="true">
|
||||
<!-- 二级侧边栏:固定在内容区左侧(独立层级) -->
|
||||
<AdminSubSider
|
||||
v-if="activeGroups.length > 0"
|
||||
:activeMenuTitle="activeMenuTitle"
|
||||
:groups="activeGroups"
|
||||
:activeSubId="activeSubId"
|
||||
@sub-click="onSubClick"
|
||||
/>
|
||||
|
||||
<!-- 右侧内容区(Header + Tags + 内容展示区 + Footer) -->
|
||||
<view
|
||||
v-for="menu in menuList"
|
||||
:key="menu.id"
|
||||
class="menu-item-primary"
|
||||
:class="{ active: activeMenu === menu.id }"
|
||||
@click="handleMenuClick(menu)"
|
||||
class="main"
|
||||
:style="{ marginLeft: activeGroups.length > 0 ? '336px' : '96px' }"
|
||||
>
|
||||
<image class="menu-icon-img" :src="menu.icon" mode="aspectFit" />
|
||||
<text class="menu-title" v-if="!isCollapsed">{{ menu.title }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<AdminHeader
|
||||
:breadcrumb="breadcrumb"
|
||||
:hasNotification="hasNotification"
|
||||
@search="onSearch"
|
||||
@refresh="onRefresh"
|
||||
@notify="onNotify"
|
||||
/>
|
||||
|
||||
<!-- 主内容区:二级侧边栏 + 右侧内容 -->
|
||||
<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>
|
||||
<AdminTagsView
|
||||
:tabs="tabs"
|
||||
:activeTabId="activeTabId"
|
||||
@tab-click="onTabClick"
|
||||
@tab-close="onTabClose"
|
||||
/>
|
||||
|
||||
<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 内容(你的页面内容都在这里展示) -->
|
||||
<scroll-view class="content" scroll-y="true">
|
||||
<view class="content-inner">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<AdminFooter />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminAside from './components/AdminAside.uvue'
|
||||
import AdminSubSider from './components/AdminSubSider.uvue'
|
||||
import AdminHeader from './components/AdminHeader.uvue'
|
||||
import AdminTagsView from './components/AdminTagsView.uvue'
|
||||
import AdminFooter from './components/AdminFooter.uvue'
|
||||
|
||||
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[] }
|
||||
import { menuList as menuConst } from './utils/menu.uts'
|
||||
import { findActiveByCurrentPage, getCurrentRoutePath } from './utils/nav.uts'
|
||||
import { makeTabFromPath, upsertTab, removeTab } from './utils/tabs.uts'
|
||||
import type { MenuItem, TabItem } from './types.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
currentPage: string
|
||||
}>()
|
||||
// 你页面传进来的 currentPage:可能是顶级 id,也可能是子页面 id(user-list)
|
||||
const props = defineProps<{ currentPage: string }>()
|
||||
|
||||
const menuList = ref<MenuItem[]>(menuConst)
|
||||
|
||||
const isCollapsed = ref(false)
|
||||
const activeMenu = ref('home')
|
||||
const activeSubMenu = ref('')
|
||||
const hasNotification = ref(true)
|
||||
|
||||
// 当前激活菜单下:展开的分组
|
||||
const openGroupIds = ref<string[]>([])
|
||||
// active states
|
||||
const activeMenuId = ref('home')
|
||||
const activeSubId = ref('')
|
||||
|
||||
// ✅ 你 static 里 6 个 svg:homepage / 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' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
// tabs
|
||||
const tabs = ref<TabItem[]>([
|
||||
{ id: 'home', title: '首页', path: '/pages/mall/admin/homePage/index' }
|
||||
])
|
||||
const activeTabId = ref('home')
|
||||
|
||||
/** ✅ 关键:把 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 : '' }
|
||||
// 每次 layout 渲染时,同步高亮(靠 currentPage)
|
||||
const syncActiveByCurrentPage = () => {
|
||||
const r = findActiveByCurrentPage(menuList.value, props.currentPage)
|
||||
activeMenuId.value = r.activeMenuId
|
||||
activeSubId.value = r.activeSubId
|
||||
}
|
||||
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
// 同步 tabs(靠当前 route)
|
||||
const syncTabsByRoute = () => {
|
||||
const path = getCurrentRoutePath()
|
||||
if (!path) return
|
||||
|
||||
const tab = makeTabFromPath(menuList.value, path)
|
||||
tabs.value = upsertTab(tabs.value, tab)
|
||||
activeTabId.value = tab.id
|
||||
}
|
||||
|
||||
// 3) 找不到:回退到首页
|
||||
return { menuId: 'home', subId: '', groupId: '' }
|
||||
}
|
||||
// 初始化同步(setup 执行一次)
|
||||
syncActiveByCurrentPage()
|
||||
syncTabsByRoute()
|
||||
|
||||
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 }
|
||||
}
|
||||
// computed
|
||||
const activeMenu = computed(() => menuList.value.find(m => m.id === activeMenuId.value))
|
||||
const activeMenuTitle = computed(() => activeMenu.value?.title || '商城后台')
|
||||
const activeGroups = computed(() => activeMenu.value?.groups || [])
|
||||
|
||||
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)
|
||||
const breadcrumb = computed(() => {
|
||||
// “一级 / 二级”
|
||||
let subTitle = ''
|
||||
const groups = activeGroups.value
|
||||
for (const g of groups) {
|
||||
const hit = g.children.find(c => c.id === activeSubId.value)
|
||||
if (hit) { subTitle = hit.title; break }
|
||||
}
|
||||
}
|
||||
|
||||
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 ''
|
||||
return subTitle ? `${activeMenuTitle.value} / ${subTitle}` : `${activeMenuTitle.value}`
|
||||
})
|
||||
|
||||
// handlers
|
||||
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 })
|
||||
// 你明确要用 navigateTo:页面栈会增长(这是正常行为):contentReference[oaicite:4]{index=4}
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
const handleMenuClick = (menu: MenuItem) => {
|
||||
activeMenu.value = menu.id
|
||||
const onMenuClick = (menuId: string) => {
|
||||
const m = menuList.value.find(x => x.id === menuId)
|
||||
if (!m) return
|
||||
activeMenuId.value = m.id
|
||||
// 默认激活该菜单第一个子项
|
||||
const g0 = (m.groups && m.groups.length > 0) ? m.groups[0] : null
|
||||
const c0 = g0 && g0.children.length > 0 ? g0.children[0] : null
|
||||
activeSubId.value = c0 ? c0.id : ''
|
||||
go(m.path)
|
||||
}
|
||||
|
||||
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 onSubClick = (child: any) => {
|
||||
activeSubId.value = child.id
|
||||
go(child.path)
|
||||
}
|
||||
|
||||
const onTabClick = (tab: TabItem) => {
|
||||
activeTabId.value = tab.id
|
||||
go(tab.path)
|
||||
}
|
||||
|
||||
const onTabClose = (tabId: string) => {
|
||||
// 关闭当前 tab:删除后回到最后一个 tab
|
||||
const wasActive = activeTabId.value === tabId
|
||||
tabs.value = removeTab(tabs.value, tabId)
|
||||
if (wasActive) {
|
||||
const last = tabs.value[tabs.value.length - 1]
|
||||
if (last) {
|
||||
activeTabId.value = last.id
|
||||
go(last.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' })
|
||||
}
|
||||
})
|
||||
}
|
||||
const onSearch = () => uni.showToast({ title: '搜索', icon: 'none' })
|
||||
const onRefresh = () => uni.showToast({ title: '刷新', icon: 'none' })
|
||||
const onNotify = () => uni.showToast({ title: '通知', icon: 'none' })
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f0f2f5;
|
||||
.layout-root{
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background:#f3f4f6;
|
||||
}
|
||||
|
||||
/* ===== 主侧边栏 ===== */
|
||||
.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;
|
||||
/* 右侧主区域:左边距由 template 动态控制(96 或 336) */
|
||||
.main{
|
||||
min-height: 100vh;
|
||||
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;
|
||||
/* 展示区 */
|
||||
.content{
|
||||
height: calc(100vh - 56px - 44px);
|
||||
}
|
||||
|
||||
.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;
|
||||
.content-inner{
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
<!-- CRMEB Admin TagsView组件 - uni-app版本 -->
|
||||
<template>
|
||||
<view class="admin-tags-view">
|
||||
<scroll-view class="tags-scroll" scroll-x="true">
|
||||
<view class="tags-container">
|
||||
<view
|
||||
v-for="(tag, index) in tagList"
|
||||
:key="tag.path"
|
||||
class="tag-item"
|
||||
:class="{ 'tag-active': activeTag === tag.path }"
|
||||
@click="handleTagClick(tag)"
|
||||
>
|
||||
<text class="tag-text">{{ tag.title }}</text>
|
||||
<view
|
||||
class="tag-close"
|
||||
v-if="!tag.isAffix"
|
||||
@click.stop="handleTagClose(tag, index)"
|
||||
>
|
||||
<text class="iconfont icon-close"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 标签页操作 -->
|
||||
<view class="tags-actions">
|
||||
<view class="action-item" @click="closeOtherTags">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</view>
|
||||
<view class="action-item" @click="closeAllTags">
|
||||
<text class="iconfont icon-clear"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import type { TagItem } from './types.uts'
|
||||
|
||||
export default {
|
||||
name: 'AdminTagsView',
|
||||
data() {
|
||||
return {
|
||||
tagList: [] as TagItem[],
|
||||
activeTag: ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.initTags()
|
||||
this.updateActiveTag()
|
||||
},
|
||||
onShow() {
|
||||
this.updateActiveTag()
|
||||
},
|
||||
methods: {
|
||||
// 初始化标签页
|
||||
initTags() {
|
||||
const savedTags = uni.getStorageSync('admin_tags')
|
||||
if (savedTags && savedTags.length > 0) {
|
||||
this.tagList = savedTags
|
||||
} else {
|
||||
// 默认标签页
|
||||
this.tagList = [
|
||||
{
|
||||
path: '/pages/mall/admin/index',
|
||||
title: '首页',
|
||||
isAffix: true // 固定标签
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 更新当前激活的标签
|
||||
updateActiveTag() {
|
||||
// 暂时禁用标签页功能,避免getCurrentPages()在组件中调用的问题
|
||||
// TODO: 通过props传入当前页面信息
|
||||
this.activeTag = '/pages/mall/admin/index' // 默认激活首页
|
||||
},
|
||||
|
||||
// 获取页面标题
|
||||
getPageTitle(path: string): string {
|
||||
const titleMap: { [key: string]: string } = {
|
||||
'/pages/mall/admin/index': '首页',
|
||||
'/pages/mall/admin/user-management': '用户管理',
|
||||
'/pages/mall/admin/product-management': '商品管理',
|
||||
'/pages/mall/admin/order-management': '订单管理',
|
||||
'/pages/mall/admin/finance-management': '财务管理',
|
||||
'/pages/mall/admin/system-settings': '系统设置'
|
||||
}
|
||||
return titleMap[path] || '页面'
|
||||
},
|
||||
|
||||
// 处理标签点击
|
||||
handleTagClick(tag: TagItem) {
|
||||
this.activeTag = tag.path
|
||||
uni.navigateTo({
|
||||
url: tag.path
|
||||
})
|
||||
},
|
||||
|
||||
// 处理标签关闭
|
||||
handleTagClose(tag: TagItem, index: number) {
|
||||
if (tag.isAffix) return // 固定标签不能关闭
|
||||
|
||||
this.tagList.splice(index, 1)
|
||||
this.saveTags()
|
||||
|
||||
// 如果关闭的是当前标签,跳转到上一个标签
|
||||
if (tag.path === this.activeTag && this.tagList.length > 0) {
|
||||
const nextTag = this.tagList[this.tagList.length - 1]
|
||||
this.handleTagClick(nextTag)
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭其他标签
|
||||
closeOtherTags() {
|
||||
const affixTags = this.tagList.filter(tag => tag.isAffix)
|
||||
const currentTag = this.tagList.find(tag => tag.path === this.activeTag)
|
||||
|
||||
if (currentTag && !currentTag.isAffix) {
|
||||
affixTags.push(currentTag)
|
||||
}
|
||||
|
||||
this.tagList = affixTags
|
||||
this.saveTags()
|
||||
},
|
||||
|
||||
// 关闭所有标签
|
||||
closeAllTags() {
|
||||
// 只保留固定标签
|
||||
this.tagList = this.tagList.filter(tag => tag.isAffix)
|
||||
this.saveTags()
|
||||
|
||||
// 如果当前页面不是固定标签,跳转到首页
|
||||
if (!this.tagList.find(tag => tag.path === this.activeTag)) {
|
||||
this.handleTagClick(this.tagList[0])
|
||||
}
|
||||
},
|
||||
|
||||
// 保存标签页到本地存储
|
||||
saveTags() {
|
||||
uni.setStorageSync('admin_tags', this.tagList)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-tags-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1rpx solid #e9ecef;
|
||||
}
|
||||
|
||||
.tags-scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40rpx;
|
||||
padding: 0 20rpx;
|
||||
background-color: #fff;
|
||||
border: 1rpx solid #dee2e6;
|
||||
border-radius: 6rpx;
|
||||
margin-right: 10rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:active {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&.tag-active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 26rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.tag-close {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
border-left: 1rpx solid #e9ecef;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6rpx;
|
||||
margin-left: 10rpx;
|
||||
transition: background-color 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media screen and (max-width: 768rpx) {
|
||||
.admin-tags-view {
|
||||
display: none; /* 移动端隐藏标签页 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,5 @@
|
||||
// 统一类型定义文件,避免重复定义冲突
|
||||
|
||||
export type MenuItem = {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
path?: string
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
export type UserInfo = {
|
||||
nickname: string
|
||||
role: string
|
||||
@@ -18,3 +10,28 @@ export type TagItem = {
|
||||
title: string
|
||||
isAffix?: boolean
|
||||
}
|
||||
|
||||
export type MenuChild = {
|
||||
id: string
|
||||
title: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export type MenuGroup = {
|
||||
title: string
|
||||
children: MenuChild[]
|
||||
}
|
||||
|
||||
export type MenuItem = {
|
||||
id: string
|
||||
title: string
|
||||
icon: string // 你的 svg 路径
|
||||
path?: string
|
||||
groups?: MenuGroup[]
|
||||
}
|
||||
|
||||
export type TabItem = {
|
||||
id: string
|
||||
title: string
|
||||
path: string
|
||||
}
|
||||
|
||||
101
layouts/admin/utils/menu.uts
Normal file
101
layouts/admin/utils/menu.uts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { MenuItem } from '../types.uts'
|
||||
|
||||
export const menuList: MenuItem[] = [
|
||||
{
|
||||
id: 'home',
|
||||
title: '首页',
|
||||
icon: '/static/homepage.svg',
|
||||
path: '/pages/mall/admin/homePage/index',
|
||||
groups: []
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: '用户',
|
||||
icon: '/static/user.svg',
|
||||
path: '/pages/mall/admin/user-management',
|
||||
groups: [
|
||||
{
|
||||
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: 'user-statistics', title: '用户统计 ', path: '/pages/mall/admin/user-statistics' },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: '订单',
|
||||
icon: '/static/order.svg',
|
||||
path: '/pages/mall/admin/order-management',
|
||||
groups: [
|
||||
{
|
||||
title: '订单管理',
|
||||
children: [
|
||||
{ id: 'order-list', title: '订单列表', path: '/pages/mall/admin/order-management' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
title: '商品',
|
||||
icon: '/static/shopping.svg',
|
||||
path: '/pages/mall/admin/product-management',
|
||||
groups: [
|
||||
{
|
||||
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: 'marketing',
|
||||
title: '营销',
|
||||
icon: '/static/finance.svg',
|
||||
path: '/pages/mall/admin/marketing-management',
|
||||
groups: [
|
||||
{
|
||||
title: '优惠券活动',
|
||||
children: [
|
||||
{ id: 'coupon-list', title: '优惠券列表', path: '/pages/mall/admin/marketing/coupon/list' }
|
||||
{ id: 'coupon-receive', title: '领取情况', path: '/pages/mall/admin/marketing/coupon/receive' }
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '积分',
|
||||
children: [
|
||||
{ id: 'points', title: '积分管理', path: '/pages/mall/admin/marketing/points/index' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '签到',
|
||||
children: [
|
||||
{ id: 'rule', title: '签到规则', path: '/pages/mall/admin/marketing/signin/rule' }
|
||||
{ id: 'record', title: '记录', path: '/pages/mall/admin/marketing/signin/record' }
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '设置',
|
||||
icon: '/static/setting.svg',
|
||||
path: '/pages/mall/admin/system-settings',
|
||||
groups: [
|
||||
{
|
||||
title: '系统设置',
|
||||
children: [
|
||||
{ id: 'basic', title: '基本设置', path: '/pages/mall/admin/system-settings' },
|
||||
{ id: 'security', title: '安全设置', path: '/pages/mall/admin/system-settings?tab=security' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
34
layouts/admin/utils/nav.uts
Normal file
34
layouts/admin/utils/nav.uts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { MenuItem } from '../types.uts'
|
||||
|
||||
export function findActiveByCurrentPage(menuList: MenuItem[], currentPage: string) {
|
||||
// currentPage 既可能是顶级菜单 id,也可能是子页面 id(如 user-list)
|
||||
// 返回:activeMenuId / activeSubId / activeGroupTitle
|
||||
for (const m of menuList) {
|
||||
if (m.id === currentPage) {
|
||||
return { activeMenuId: m.id, activeSubId: '', activeGroupTitle: '' }
|
||||
}
|
||||
const groups = m.groups || []
|
||||
for (const g of groups) {
|
||||
for (const c of g.children) {
|
||||
if (c.id === currentPage) {
|
||||
return { activeMenuId: m.id, activeSubId: c.id, activeGroupTitle: g.title }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { activeMenuId: menuList[0]?.id || 'home', activeSubId: '', activeGroupTitle: '' }
|
||||
}
|
||||
|
||||
export function getCurrentRoutePath(): string {
|
||||
// 使用页面栈获取当前路由(uni-app标准能力)
|
||||
// getCurrentPages 用于获取当前页面栈实例 :contentReference[oaicite:2]{index=2}
|
||||
const pages = getCurrentPages()
|
||||
const last: any = pages[pages.length - 1]
|
||||
// #ifdef H5
|
||||
return last?.route ? `/${last.route}` : ''
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// 小程序/App 可能是 route / $page?.fullPath 形式,按你项目实际字段微调
|
||||
return last?.route ? `/${last.route}` : (last?.$page?.fullPath || '')
|
||||
// #endif
|
||||
}
|
||||
33
layouts/admin/utils/tabs.uts
Normal file
33
layouts/admin/utils/tabs.uts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { TabItem, MenuItem } from '../types.uts'
|
||||
|
||||
export function makeTabFromPath(menuList: MenuItem[], path: string): TabItem {
|
||||
// path 可能带 query;用于 tab 的 id 也要稳定
|
||||
const pure = path.split('?')[0]
|
||||
|
||||
// 先找子页面
|
||||
for (const m of menuList) {
|
||||
const groups = m.groups || []
|
||||
for (const g of groups) {
|
||||
for (const c of g.children) {
|
||||
if (c.path.split('?')[0] === pure) {
|
||||
return { id: c.id, title: c.title, path: c.path }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m.path.split('?')[0] === pure) {
|
||||
return { id: m.id, title: m.title, path: m.path }
|
||||
}
|
||||
}
|
||||
// 找不到就兜底
|
||||
return { id: pure, title: '页面', path }
|
||||
}
|
||||
|
||||
export function upsertTab(tabs: TabItem[], tab: TabItem): TabItem[] {
|
||||
const idx = tabs.findIndex(t => t.id === tab.id)
|
||||
if (idx >= 0) return tabs
|
||||
return [...tabs, tab]
|
||||
}
|
||||
|
||||
export function removeTab(tabs: TabItem[], tabId: string): TabItem[] {
|
||||
return tabs.filter(t => t.id !== tabId)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/mall/admin/index",
|
||||
"path": "pages/mall/admin/homePage/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "管理后台",
|
||||
"navigationStyle": "custom"
|
||||
|
||||
187
pages/mall/admin/homePage/components/KpiMiniCard.uvue
Normal file
187
pages/mall/admin/homePage/components/KpiMiniCard.uvue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="kpi-card">
|
||||
<!-- Header -->
|
||||
<view class="kpi-header">
|
||||
<text class="kpi-title">{{ title }}</text>
|
||||
|
||||
<view v-if="tagText" class="kpi-tag">
|
||||
<text class="kpi-tag-text">{{ tagText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 可选:你想在右上角塞额外按钮/图标 -->
|
||||
<slot name="headerRight"></slot>
|
||||
</view>
|
||||
|
||||
<!-- Body -->
|
||||
<view class="kpi-body">
|
||||
<text class="kpi-main-value">{{ valuePrefix }}{{ valueText }}</text>
|
||||
|
||||
<!-- 中间“昨日 / 日环比”行(可完全替换) -->
|
||||
<view v-if="metaLeft || metaRight" class="kpi-meta">
|
||||
<text v-if="metaLeft" class="kpi-meta-text">{{ metaLeft }}</text>
|
||||
|
||||
<view v-if="metaRight" class="kpi-meta-right">
|
||||
<text class="kpi-meta-text">{{ metaRight }}</text>
|
||||
|
||||
<text
|
||||
v-if="trend !== 'none'"
|
||||
class="kpi-trend-arrow"
|
||||
:class="trendClass"
|
||||
>
|
||||
{{ trendArrow }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 可选:完全自定义这行 -->
|
||||
<slot name="meta"></slot>
|
||||
</view>
|
||||
|
||||
<view class="kpi-divider"></view>
|
||||
|
||||
<!-- 底部一行:左文案 + 右数值 -->
|
||||
<view class="kpi-footer">
|
||||
<text class="kpi-footer-left">{{ footerLeftText }}</text>
|
||||
<text class="kpi-footer-right">{{ footerRightText }}</text>
|
||||
|
||||
<!-- 可选:完全自定义 footer -->
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
// Header
|
||||
title: string
|
||||
tagText?: string
|
||||
|
||||
// Body main
|
||||
valueText: string
|
||||
valuePrefix?: string // 例如 "¥"
|
||||
|
||||
// Meta line (可替换)
|
||||
metaLeft?: string // 例如 "昨日 4"
|
||||
metaRight?: string // 例如 "日环比 0%"
|
||||
trend?: 'up' | 'down' | 'flat' | 'none' // none = 不显示箭头
|
||||
|
||||
// Footer
|
||||
footerLeftText: string // 例如 "本月订单量"
|
||||
footerRightText: string // 例如 "181单"
|
||||
}>(), {
|
||||
tagText: '今日',
|
||||
valuePrefix: '',
|
||||
metaLeft: '',
|
||||
metaRight: '',
|
||||
trend: 'none'
|
||||
})
|
||||
|
||||
const trendArrow = computed((): string => {
|
||||
if (props.trend === 'up') return '▲'
|
||||
if (props.trend === 'down') return '▼'
|
||||
return '•'
|
||||
})
|
||||
|
||||
const trendClass = computed((): string => {
|
||||
if (props.trend === 'up') return 'is-up'
|
||||
if (props.trend === 'down') return 'is-down'
|
||||
return 'is-flat'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.kpi-card{
|
||||
background-color:#ffffff;
|
||||
border:1px solid #ebeef5;
|
||||
border-radius:6px;
|
||||
padding:16px;
|
||||
box-shadow:0 2px 12px rgba(0,0,0,0.04);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.kpi-header{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
|
||||
.kpi-title{
|
||||
font-size:14px;
|
||||
color:#303133;
|
||||
font-weight:600;
|
||||
}
|
||||
.kpi-tag{
|
||||
padding:2px 8px;
|
||||
border-radius:4px;
|
||||
border:1px solid #e1f3d8;
|
||||
background:#f0f9eb;
|
||||
}
|
||||
.kpi-tag-text{
|
||||
font-size:12px;
|
||||
color:#67c23a;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Body */
|
||||
.kpi-body{
|
||||
margin-top:10px;
|
||||
.kpi-main-value{
|
||||
font-size:32px;
|
||||
font-weight:600;
|
||||
color:#303133;
|
||||
line-height:40px;
|
||||
}
|
||||
|
||||
/* “昨日 / 日环比” */
|
||||
.kpi-meta{
|
||||
margin-top:8px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:flex-start;
|
||||
gap:12px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.kpi-meta-text{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
.kpi-meta-right{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
}
|
||||
.kpi-trend-arrow{
|
||||
font-size:12px;
|
||||
}
|
||||
.kpi-trend-arrow.is-up{ color:#f56c6c; }
|
||||
.kpi-trend-arrow.is-down{ color:#67c23a; }
|
||||
.kpi-trend-arrow.is-flat{ color:#909399; }
|
||||
|
||||
.kpi-divider{
|
||||
height:1px;
|
||||
background:#ebeef5;
|
||||
margin:12px 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.kpi-footer{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
gap:12px;
|
||||
}
|
||||
.kpi-footer-left{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
.kpi-footer-right{
|
||||
font-size:12px;
|
||||
color:#909399;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -3,116 +3,58 @@
|
||||
<view class="dashboard-page">
|
||||
<!-- 第一行:4 个 KPI 卡片 -->
|
||||
<view class="kpi-cards-row">
|
||||
<!-- 销售额卡片 -->
|
||||
<view class="kpi-card">
|
||||
<view class="kpi-card-content">
|
||||
<view class="kpi-card-header">
|
||||
<text class="kpi-card-title">销售额</text>
|
||||
<view class="kpi-card-tag">
|
||||
<text class="kpi-tag-text">今日</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-value">
|
||||
<text class="kpi-value-number">¥{{ formatNumber(kpiData.sales.today) }}</text>
|
||||
<view class="kpi-value-trend" :class="{ 'up': kpiData.sales.change > 0, 'down': kpiData.sales.change < 0 }">
|
||||
<text class="iconfont" :class="{ 'icon-up': kpiData.sales.change > 0, 'icon-down': kpiData.sales.change < 0 }"></text>
|
||||
<text class="kpi-trend-text">{{ Math.abs(kpiData.sales.change) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-footer">
|
||||
<text class="kpi-footer-text">昨日:¥{{ formatNumber(kpiData.sales.yesterday) }}</text>
|
||||
<text class="kpi-footer-text">本月累计:¥{{ formatNumber(kpiData.sales.monthTotal) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-icon">
|
||||
<text class="iconfont icon-sales"></text>
|
||||
</view>
|
||||
<KpiMiniCard
|
||||
title="销售额"
|
||||
tagText="今日"
|
||||
valuePrefix="¥"
|
||||
:valueText="String(formatNumber(kpiData.sales.today))"
|
||||
:metaLeft="`昨日 ${formatNumber(kpiData.sales.yesterday)}`"
|
||||
:metaRight="`日环比 ${Math.abs(kpiData.sales.change)}%`"
|
||||
:trend="kpiData.sales.change > 0 ? 'up' : (kpiData.sales.change < 0 ? 'down' : 'flat')"
|
||||
:footerLeftText="'本月销售额'"
|
||||
:footerRightText="`¥${formatNumber(kpiData.sales.monthTotal)}`"
|
||||
/>
|
||||
|
||||
<KpiMiniCard
|
||||
title="用户访问量"
|
||||
tagText="今日"
|
||||
:valueText="String(formatNumber(kpiData.visits.today))"
|
||||
:metaLeft="`昨日 ${formatNumber(kpiData.visits.yesterday)}`"
|
||||
:metaRight="`日环比 ${Math.abs(kpiData.visits.change)}%`"
|
||||
:trend="kpiData.visits.change > 0 ? 'up' : (kpiData.visits.change < 0 ? 'down' : 'flat')"
|
||||
footerLeftText="本月访问量"
|
||||
:footerRightText="`${formatNumber(kpiData.visits.monthTotal)}Pv`"
|
||||
/>
|
||||
|
||||
<KpiMiniCard
|
||||
title="订单量"
|
||||
tagText="今日"
|
||||
:valueText="String(formatNumber(kpiData.orders.today))"
|
||||
:metaLeft="`昨日 ${formatNumber(kpiData.orders.yesterday)}`"
|
||||
:metaRight="`日环比 ${Math.abs(kpiData.orders.change)}%`"
|
||||
:trend="kpiData.orders.change > 0 ? 'up' : (kpiData.orders.change < 0 ? 'down' : 'flat')"
|
||||
footerLeftText="本月订单量"
|
||||
:footerRightText="`${formatNumber(kpiData.orders.monthTotal)}单`"
|
||||
/>
|
||||
|
||||
<KpiMiniCard
|
||||
title="新增用户"
|
||||
tagText="今日"
|
||||
:valueText="String(formatNumber(kpiData.users.today))"
|
||||
:metaLeft="`昨日 ${formatNumber(kpiData.users.yesterday)}`"
|
||||
:metaRight="`日环比 ${Math.abs(kpiData.users.change)}%`"
|
||||
:trend="kpiData.users.change > 0 ? 'up' : (kpiData.users.change < 0 ? 'down' : 'flat')"
|
||||
footerLeftText="本月新增用户"
|
||||
:footerRightText="`${formatNumber(kpiData.users.monthTotal)}人`"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 访问量卡片 -->
|
||||
<view class="kpi-card">
|
||||
<view class="kpi-card-content">
|
||||
<view class="kpi-card-header">
|
||||
<text class="kpi-card-title">访问量</text>
|
||||
<view class="kpi-card-tag">
|
||||
<text class="kpi-tag-text">今日</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-value">
|
||||
<text class="kpi-value-number">{{ formatNumber(kpiData.visits.today) }}</text>
|
||||
<view class="kpi-value-trend" :class="{ 'up': kpiData.visits.change > 0, 'down': kpiData.visits.change < 0 }">
|
||||
<text class="iconfont" :class="{ 'icon-up': kpiData.visits.change > 0, 'icon-down': kpiData.visits.change < 0 }"></text>
|
||||
<text class="kpi-trend-text">{{ Math.abs(kpiData.visits.change) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-footer">
|
||||
<text class="kpi-footer-text">昨日:{{ formatNumber(kpiData.visits.yesterday) }}</text>
|
||||
<text class="kpi-footer-text">本月累计:{{ formatNumber(kpiData.visits.monthTotal) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-icon">
|
||||
<text class="iconfont icon-visits"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订单量卡片 -->
|
||||
<view class="kpi-card">
|
||||
<view class="kpi-card-content">
|
||||
<view class="kpi-card-header">
|
||||
<text class="kpi-card-title">订单量</text>
|
||||
<view class="kpi-card-tag">
|
||||
<text class="kpi-tag-text">今日</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-value">
|
||||
<text class="kpi-value-number">{{ formatNumber(kpiData.orders.today) }}</text>
|
||||
<view class="kpi-value-trend" :class="{ 'up': kpiData.orders.change > 0, 'down': kpiData.orders.change < 0 }">
|
||||
<text class="iconfont" :class="{ 'icon-up': kpiData.orders.change > 0, 'icon-down': kpiData.orders.change < 0 }"></text>
|
||||
<text class="kpi-trend-text">{{ Math.abs(kpiData.orders.change) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-footer">
|
||||
<text class="kpi-footer-text">昨日:{{ formatNumber(kpiData.orders.yesterday) }}</text>
|
||||
<text class="kpi-footer-text">本月累计:{{ formatNumber(kpiData.orders.monthTotal) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-icon">
|
||||
<text class="iconfont icon-orders"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增用户卡片 -->
|
||||
<view class="kpi-card">
|
||||
<view class="kpi-card-content">
|
||||
<view class="kpi-card-header">
|
||||
<text class="kpi-card-title">新增用户</text>
|
||||
<view class="kpi-card-tag">
|
||||
<text class="kpi-tag-text">今日</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-value">
|
||||
<text class="kpi-value-number">{{ formatNumber(kpiData.users.today) }}</text>
|
||||
<view class="kpi-value-trend" :class="{ 'up': kpiData.users.change > 0, 'down': kpiData.users.change < 0 }">
|
||||
<text class="iconfont" :class="{ 'icon-up': kpiData.users.change > 0, 'icon-down': kpiData.users.change < 0 }"></text>
|
||||
<text class="kpi-trend-text">{{ Math.abs(kpiData.users.change) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-footer">
|
||||
<text class="kpi-footer-text">昨日:{{ formatNumber(kpiData.users.yesterday) }}</text>
|
||||
<text class="kpi-footer-text">本月累计:{{ formatNumber(kpiData.users.monthTotal) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="kpi-card-icon">
|
||||
<text class="iconfont icon-users"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:订单统计图表 -->
|
||||
<view class="chart-section">
|
||||
<view class="admin-card">
|
||||
<view class="admin-card-header">
|
||||
<text class="admin-card-title">订单统计</text>
|
||||
<text class="admin-card-title">订单</text>
|
||||
<view class="chart-controls">
|
||||
<button
|
||||
v-for="period in chartPeriods"
|
||||
@@ -172,6 +114,7 @@
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/layouts/admin/index.uvue'
|
||||
import KpiMiniCard from './components/KpiMiniCard.uvue'
|
||||
|
||||
// KPI 数据
|
||||
const kpiData = ref({
|
||||
@@ -244,39 +187,60 @@ const changePeriod = (period: string) => {
|
||||
}
|
||||
|
||||
/* ===== KPI 卡片行 ===== */
|
||||
/* 第一行:4 个 KPI 卡片一行 */
|
||||
.kpi-cards-row{
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 一行 4 列等分 */
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 卡片本体:不要写死宽高 */
|
||||
.kpi-card{
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
height: 200px; /* 你可以改成 140/160,别写死 200px */
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
/* 响应式:宽度不够时变 2 列 / 1 列(可选) */
|
||||
@media (max-width: 1200px){
|
||||
.kpi-cards-row{ grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 768px){
|
||||
.kpi-cards-row{ grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
|
||||
.kpi-card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kpi-card-title {
|
||||
position: absolute;
|
||||
|
||||
top: 10rpx;
|
||||
left: 5rpx;
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
margin-right: 12px;
|
||||
@@ -381,7 +345,6 @@ const changePeriod = (period: string) => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.admin-card-title {
|
||||
font-size: 18px;
|
||||
@@ -392,10 +355,14 @@ const changePeriod = (period: string) => {
|
||||
.admin-card-body {
|
||||
padding: 0 24px 24px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ===== 图表控件 ===== */
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
<template>
|
||||
<view class="marketing-management">
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-header">
|
||||
<text class="page-title">营销管理</text>
|
||||
<text class="page-subtitle">管理营销活动、优惠券、直播等营销工具</text>
|
||||
</view>
|
||||
|
||||
<!-- 营销工具导航 -->
|
||||
<view class="marketing-tools">
|
||||
<view class="tool-grid">
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/store-coupon')">
|
||||
<view class="tool-icon">🎫</view>
|
||||
<text class="tool-name">优惠券管理</text>
|
||||
<text class="tool-desc">创建和管理优惠券活动</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/store-seckill')">
|
||||
<view class="tool-icon">⚡</view>
|
||||
<text class="tool-name">秒杀活动</text>
|
||||
<text class="tool-desc">管理限时秒杀活动</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/store-bargain')">
|
||||
<view class="tool-icon">🪓</view>
|
||||
<text class="tool-name">砍价活动</text>
|
||||
<text class="tool-desc">管理砍价促销活动</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/store-combination')">
|
||||
<view class="tool-icon">👥</view>
|
||||
<text class="tool-name">拼团活动</text>
|
||||
<text class="tool-desc">管理拼团购买活动</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/live-broadcast')">
|
||||
<view class="tool-icon">📺</view>
|
||||
<text class="tool-name">直播管理</text>
|
||||
<text class="tool-desc">管理直播带货活动</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/channel-code')">
|
||||
<view class="tool-icon">📱</view>
|
||||
<text class="tool-name">渠道码</text>
|
||||
<text class="tool-desc">生成和管理推广渠道码</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/store-integral')">
|
||||
<view class="tool-icon">💎</view>
|
||||
<text class="tool-name">积分商品</text>
|
||||
<text class="tool-desc">管理积分兑换商品</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="go('/pages/mall/admin/sign-in')">
|
||||
<view class="tool-icon">📅</view>
|
||||
<text class="tool-name">签到活动</text>
|
||||
<text class="tool-desc">管理用户签到奖励</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 营销数据统计 -->
|
||||
<view class="marketing-stats">
|
||||
<text class="section-title">营销数据概览</text>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.totalCoupons }}</text>
|
||||
<text class="stat-label">优惠券发放</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.activeActivities }}</text>
|
||||
<text class="stat-label">进行中活动</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.totalParticipants }}</text>
|
||||
<text class="stat-label">活动参与人数</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">¥{{ stats.marketingRevenue }}</text>
|
||||
<text class="stat-label">营销收入</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="quick-actions">
|
||||
<text class="section-title">快捷操作</text>
|
||||
<view class="action-buttons">
|
||||
<button class="action-btn primary" @click="createActivity">创建新活动</button>
|
||||
<button class="action-btn secondary" @click="viewReports">查看营销报表</button>
|
||||
<button class="action-btn info" @click="manageCoupons">批量管理优惠券</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 响应式数据
|
||||
const stats = ref({
|
||||
totalCoupons: 0,
|
||||
activeActivities: 0,
|
||||
totalParticipants: 0,
|
||||
marketingRevenue: '0.00'
|
||||
})
|
||||
|
||||
// 统一的导航方法
|
||||
const go = (url: string) => {
|
||||
// 1) 目标页面必须是非 tabBar 页面
|
||||
// 2) 必须在 pages.json / subPackages 注册
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 快捷操作方法
|
||||
const createActivity = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['优惠券活动', '秒杀活动', '砍价活动', '拼团活动', '直播活动'],
|
||||
success: (res) => {
|
||||
const activities = [
|
||||
'/pages/mall/admin/store-coupon',
|
||||
'/pages/mall/admin/store-seckill',
|
||||
'/pages/mall/admin/store-bargain',
|
||||
'/pages/mall/admin/store-combination',
|
||||
'/pages/mall/admin/live-broadcast'
|
||||
]
|
||||
if (activities[res.tapIndex]) {
|
||||
go(activities[res.tapIndex])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const viewReports = () => {
|
||||
uni.showToast({
|
||||
title: '营销报表功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const manageCoupons = () => {
|
||||
go('/pages/mall/admin/store-coupon')
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
loadMarketingStats()
|
||||
})
|
||||
|
||||
// 加载营销统计数据
|
||||
const loadMarketingStats = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的API
|
||||
// 暂时使用模拟数据
|
||||
stats.value = {
|
||||
totalCoupons: 12580,
|
||||
activeActivities: 8,
|
||||
totalParticipants: 45620,
|
||||
marketingRevenue: '128500.00'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载营销统计失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.marketing-management {
|
||||
padding: 30rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
// 页面头部
|
||||
.page-header {
|
||||
background-color: #fff;
|
||||
padding: 40rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.page-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
display: block;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
// 营销工具网格
|
||||
.marketing-tools {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.tool-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
width: calc(50% - 10rpx);
|
||||
background-color: #f8f9fa;
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #e3f2fd;
|
||||
transform: translateY(-2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tool-icon {
|
||||
font-size: 48rpx;
|
||||
margin-bottom: 15rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
margin-bottom: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
font-size: 22rpx;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 营销数据统计
|
||||
.marketing-stats {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
margin-bottom: 30rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 30rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
color: white;
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷操作
|
||||
.quick-actions {
|
||||
background-color: #fff;
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #212529;
|
||||
margin-bottom: 30rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
min-width: 200rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 750rpx) {
|
||||
.tool-grid {
|
||||
flex-direction: column;
|
||||
|
||||
.tool-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
28
pages/mall/admin/marketing/coupon/list.uvue
Normal file
28
pages/mall/admin/marketing/coupon/list.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<text class="title">优惠券列表</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Minimal script
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
28
pages/mall/admin/marketing/coupon/receive.uvue
Normal file
28
pages/mall/admin/marketing/coupon/receive.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<text class="title">用户领取记录</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Minimal script
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
28
pages/mall/admin/marketing/points/index.uvue
Normal file
28
pages/mall/admin/marketing/points/index.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<text class="title">积分管理</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Minimal script
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
28
pages/mall/admin/marketing/signin/record.uvue
Normal file
28
pages/mall/admin/marketing/signin/record.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<text class="title">签到记录</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Minimal script
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
28
pages/mall/admin/marketing/signin/rule.uvue
Normal file
28
pages/mall/admin/marketing/signin/rule.uvue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<view class="container">
|
||||
<text class="title">签到规则</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
// Minimal script
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -403,6 +403,41 @@
|
||||
"navigationBarTitleText": "优惠券管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "marketing/coupon/list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "优惠券列表",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "marketing/coupon/receive",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户领取记录",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "marketing/points/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "积分管理",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "marketing/signin/rule",
|
||||
"style": {
|
||||
"navigationBarTitleText": "签到规则",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "marketing/signin/record",
|
||||
"style": {
|
||||
"navigationBarTitleText": "签到记录",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "delivery-management",
|
||||
"style": {
|
||||
@@ -456,6 +491,12 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "投诉处理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "homePage/components/KpiMiniCard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "卡片模板"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user