初步构建起页面布局
This commit is contained in:
217
layouts/admin/README.md
Normal file
217
layouts/admin/README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Mall Admin 布局系统
|
||||
|
||||
基于CRMEB Admin的设计,创建的uni-app版本的管理后台布局系统。
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
layouts/admin/
|
||||
├── index.uvue # 主布局组件(入口)
|
||||
├── defaults.uvue # 默认布局(完整布局)
|
||||
├── aside.uvue # 侧边栏组件
|
||||
├── header.uvue # 顶部栏组件
|
||||
├── breadcrumb.uvue # 面包屑导航组件
|
||||
├── tags-view.uvue # 标签页组件(可选)
|
||||
└── README.md # 使用说明
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 在页面中使用布局
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<AdminLayout current-page="page-id">
|
||||
<!-- 你的页面内容 -->
|
||||
<view class="page-container">
|
||||
<text>页面内容</text>
|
||||
</view>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import AdminLayout from '@/layouts/admin/index.uvue'
|
||||
|
||||
// 页面逻辑
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 页面配置
|
||||
|
||||
在 `pages.json` 中,所有admin页面都需要设置:
|
||||
|
||||
```json
|
||||
{
|
||||
"path": "admin/your-page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "页面标题",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📋 组件说明
|
||||
|
||||
### AdminLayout (主组件)
|
||||
- **用途**: 统一的admin布局入口
|
||||
- **属性**:
|
||||
- `current-page`: 当前页面ID,用于菜单高亮
|
||||
|
||||
### AdminDefaults (默认布局)
|
||||
- **用途**: 完整的页面布局容器
|
||||
- **功能**: 包含侧边栏、主内容区、响应式适配
|
||||
|
||||
### AdminAside (侧边栏)
|
||||
- **用途**: 垂直菜单栏
|
||||
- **功能**:
|
||||
- 菜单折叠/展开
|
||||
- 子菜单支持
|
||||
- 移动端抽屉模式
|
||||
|
||||
### AdminHeader (顶部栏)
|
||||
- **用途**: 页面头部导航
|
||||
- **功能**: 面包屑导航、用户信息、通知中心
|
||||
|
||||
### AdminBreadcrumb (面包屑)
|
||||
- **用途**: 页面导航指示器
|
||||
- **功能**: 显示当前页面位置、快速导航
|
||||
|
||||
## 🎨 菜单配置
|
||||
|
||||
菜单配置在 `defaults.uvue` 中:
|
||||
|
||||
```javascript
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 📱 响应式特性
|
||||
|
||||
### 桌面端 (> 768px)
|
||||
- 侧边栏默认展开 (240rpx)
|
||||
- 支持折叠到 80rpx
|
||||
- 完整菜单显示
|
||||
|
||||
### 平板端 (600px - 768px)
|
||||
- 侧边栏可折叠
|
||||
- 菜单文本正常显示
|
||||
|
||||
### 移动端 (< 600px)
|
||||
- 侧边栏隐藏
|
||||
- 点击菜单按钮显示抽屉
|
||||
- 带背景遮罩
|
||||
|
||||
## 🎯 功能特性
|
||||
|
||||
- ✅ **垂直菜单布局** - 参考CRMEB Admin设计
|
||||
- ✅ **菜单折叠** - 支持展开/收起
|
||||
- ✅ **子菜单支持** - 多级菜单结构
|
||||
- ✅ **路由联动** - 自动高亮当前菜单
|
||||
- ✅ **响应式设计** - 适配各种屏幕尺寸
|
||||
- ✅ **移动端适配** - 抽屉式菜单
|
||||
- ✅ **主题定制** - 支持样式调整
|
||||
|
||||
## 🔧 自定义配置
|
||||
|
||||
### 修改菜单
|
||||
编辑 `defaults.uvue` 中的 `menuList` 数组
|
||||
|
||||
### 调整样式
|
||||
修改各组件的 `<style>` 部分
|
||||
|
||||
### 添加新页面
|
||||
1. 在菜单配置中添加新项
|
||||
2. 创建页面文件,使用 `AdminLayout` 包装
|
||||
3. 在 `pages.json` 中注册
|
||||
|
||||
## 📋 页面ID对照表
|
||||
|
||||
| 页面ID | 对应页面 | 说明 |
|
||||
|--------|----------|------|
|
||||
| dashboard | 首页 | 主页 |
|
||||
| user-list | 用户管理 | 用户列表页 |
|
||||
| merchant-list | 商家管理 | 商家列表页 |
|
||||
| product-list | 商品管理 | 商品列表页 |
|
||||
| order | 订单管理 | 订单管理页 |
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### 菜单不显示
|
||||
- 检查 `current-page` 属性是否正确
|
||||
- 确认菜单ID与页面ID匹配
|
||||
|
||||
### 样式异常
|
||||
- 确保页面设置了 `"navigationStyle": "custom"`
|
||||
- 检查组件导入是否正确
|
||||
|
||||
### 移动端异常
|
||||
- 确认响应式断点设置正确
|
||||
- 检查抽屉菜单逻辑
|
||||
|
||||
## 🔄 迁移指南
|
||||
|
||||
### 从旧布局迁移
|
||||
|
||||
1. **导入新布局**
|
||||
```javascript
|
||||
import AdminLayout from '@/layouts/admin/index.uvue'
|
||||
```
|
||||
|
||||
2. **包装页面内容**
|
||||
```vue
|
||||
<template>
|
||||
<AdminLayout current-page="page-id">
|
||||
<!-- 原有内容 -->
|
||||
</AdminLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
3. **更新配置**
|
||||
```json
|
||||
{
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 样式变量
|
||||
|
||||
```scss
|
||||
// 主题色
|
||||
$primary-color: #1890ff;
|
||||
$sidebar-bg: #001529;
|
||||
$navbar-bg: #ffffff;
|
||||
|
||||
// 文字颜色
|
||||
$text-primary: #333333;
|
||||
$text-secondary: rgba(255, 255, 255, 0.75);
|
||||
$text-muted: rgba(255, 255, 255, 0.65);
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请检查:
|
||||
1. 组件导入是否正确
|
||||
2. 页面配置是否完整
|
||||
3. 菜单ID是否匹配
|
||||
4. 样式冲突是否解决
|
||||
598
layouts/admin/aside.uvue
Normal file
598
layouts/admin/aside.uvue
Normal file
@@ -0,0 +1,598 @@
|
||||
<!-- 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>
|
||||
281
layouts/admin/breadcrumb.uvue
Normal file
281
layouts/admin/breadcrumb.uvue
Normal file
@@ -0,0 +1,281 @@
|
||||
<!-- 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>
|
||||
94
layouts/admin/components/card.uvue
Normal file
94
layouts/admin/components/card.uvue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<view class="admin-card" :class="cardClass">
|
||||
<view class="card-header" v-if="title || $slots.header">
|
||||
<view class="card-title-section">
|
||||
<text class="card-title">{{ title }}</text>
|
||||
<view class="card-extra" v-if="$slots.extra">
|
||||
<slot name="extra"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
bordered?: boolean
|
||||
shadow?: string
|
||||
bodyStyle?: Record<string, any>
|
||||
}>()
|
||||
|
||||
// Computed
|
||||
const cardClass = computed(() => {
|
||||
return {
|
||||
'card-bordered': props.bordered !== false,
|
||||
'card-shadow': props.shadow !== 'none',
|
||||
'card-shadow-small': props.shadow === 'small',
|
||||
'card-shadow-medium': props.shadow === 'medium',
|
||||
'card-shadow-large': props.shadow === 'large'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.admin-card {
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
|
||||
&.card-bordered {
|
||||
border: 1rpx solid #e8e8e8;
|
||||
}
|
||||
|
||||
&.card-shadow {
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&.card-shadow-small {
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&.card-shadow-medium {
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&.card-shadow-large {
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.card-title-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-extra {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 32rpx;
|
||||
}
|
||||
</style>
|
||||
453
layouts/admin/defaults.uvue
Normal file
453
layouts/admin/defaults.uvue
Normal file
@@ -0,0 +1,453 @@
|
||||
<!-- 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>
|
||||
61
layouts/admin/header.uvue
Normal file
61
layouts/admin/header.uvue
Normal file
@@ -0,0 +1,61 @@
|
||||
<!-- 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>
|
||||
351
layouts/admin/index-simple.uvue
Normal file
351
layouts/admin/index-simple.uvue
Normal file
@@ -0,0 +1,351 @@
|
||||
<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>
|
||||
703
layouts/admin/index.uvue
Normal file
703
layouts/admin/index.uvue
Normal file
@@ -0,0 +1,703 @@
|
||||
<template>
|
||||
<view class="admin-layout">
|
||||
<!-- 左侧固定深色 Sider -->
|
||||
<view class="admin-sider" :class="{ 'sider-collapsed': isCollapsed }">
|
||||
<!-- Logo区域 -->
|
||||
<view class="sider-header">
|
||||
<view class="logo">
|
||||
<text class="logo-text">{{ isCollapsed ? 'M' : 'Mall Admin' }}</text>
|
||||
</view>
|
||||
<view class="collapse-btn" @click="toggleCollapse">
|
||||
<text class="iconfont">{{ isCollapsed ? 'icon-menu-unfold' : 'icon-menu-fold' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一级菜单 (icon bar) -->
|
||||
<view class="menu-primary">
|
||||
<view
|
||||
v-for="menu in menuList"
|
||||
:key="menu.id"
|
||||
class="menu-item-primary"
|
||||
:class="{ 'active': activeMenu === menu.id }"
|
||||
@click="handleMenuClick(menu)"
|
||||
>
|
||||
<text class="iconfont menu-icon">{{ menu.icon }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 二级菜单栏 (当前一级下的文字菜单) -->
|
||||
<view class="menu-secondary" v-if="!isCollapsed">
|
||||
<view class="secondary-header">
|
||||
<text class="secondary-title">{{ activeMenuTitle }}</text>
|
||||
</view>
|
||||
<view class="secondary-content">
|
||||
<view
|
||||
v-for="subMenu in activeSubMenus"
|
||||
:key="subMenu.id"
|
||||
class="sub-menu-item"
|
||||
:class="{ 'active': activeSubMenu === subMenu.id }"
|
||||
@click="handleSubMenuClick(subMenu)"
|
||||
>
|
||||
<text class="sub-menu-text">{{ subMenu.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="admin-main" :class="{ 'main-collapsed': isCollapsed }">
|
||||
<!-- 顶部 Header -->
|
||||
<view class="admin-header">
|
||||
<view class="header-left">
|
||||
<view class="breadcrumb">
|
||||
<text class="breadcrumb-item">{{ currentPageTitle }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="header-action" @click="handleSearch">
|
||||
<text class="iconfont icon-search"></text>
|
||||
</view>
|
||||
<view class="header-action" @click="handleNotification">
|
||||
<text class="iconfont icon-bell"></text>
|
||||
<view class="notification-dot" v-if="hasNotification"></view>
|
||||
</view>
|
||||
<view class="header-action" @click="handleFullscreen">
|
||||
<text class="iconfont icon-fullscreen"></text>
|
||||
</view>
|
||||
<view class="header-user" @click="handleUserMenu">
|
||||
<image class="user-avatar" src="/static/avatar/default.png" />
|
||||
<text class="user-name">管理员</text>
|
||||
<text class="iconfont icon-down"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 多标签页 Tab Bar -->
|
||||
<view class="admin-tabs">
|
||||
<scroll-view class="tabs-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="tabs-container">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="tab-item"
|
||||
:class="{ 'active': activeTab === tab.id }"
|
||||
@click="switchTab(tab.id)"
|
||||
>
|
||||
<text class="tab-title">{{ tab.title }}</text>
|
||||
<view class="tab-close" v-if="tabs.length > 1" @click.stop="closeTab(tab.id)">
|
||||
<text class="iconfont icon-close"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容区 (flex:1 height:0 scroll-view) -->
|
||||
<scroll-view class="page-content" scroll-y="true" :style="{ flex: '1', height: '0' }">
|
||||
<view class="page-wrapper">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
currentPage: string
|
||||
}>()
|
||||
|
||||
// 响应式数据
|
||||
const isCollapsed = ref(false)
|
||||
const activeMenu = ref('dashboard')
|
||||
const activeSubMenu = ref('')
|
||||
const hasNotification = ref(true)
|
||||
|
||||
// 标签页数据
|
||||
const tabs = ref([
|
||||
{ id: 'dashboard', title: '首页', closable: false }
|
||||
])
|
||||
const activeTab = ref('dashboard')
|
||||
|
||||
// 菜单数据 (CRMEB 风格)
|
||||
const menuList = ref([
|
||||
{
|
||||
id: 'dashboard',
|
||||
title: '首页',
|
||||
icon: 'icon-dashboard',
|
||||
path: '/pages/mall/admin/index',
|
||||
subMenus: []
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
title: '用户管理',
|
||||
icon: 'icon-user',
|
||||
path: '/pages/mall/admin/user-management',
|
||||
subMenus: [
|
||||
{ id: 'user-list', title: '用户列表', path: '/pages/mall/admin/user-management' },
|
||||
{ id: 'user-add', title: '添加用户', path: '/pages/mall/admin/user-management?action=add' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'product',
|
||||
title: '商品管理',
|
||||
icon: 'icon-shopping',
|
||||
path: '/pages/mall/admin/product-management',
|
||||
subMenus: [
|
||||
{ id: 'product-list', title: '商品列表', path: '/pages/mall/admin/product-management' },
|
||||
{ id: 'product-add', title: '添加商品', path: '/pages/mall/admin/product-management?action=add' },
|
||||
{ id: 'category', title: '商品分类', path: '/pages/mall/admin/product-management?tab=category' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: '订单管理',
|
||||
icon: 'icon-order',
|
||||
path: '/pages/mall/admin/order-management',
|
||||
subMenus: [
|
||||
{ id: 'order-list', title: '订单列表', path: '/pages/mall/admin/order-management' },
|
||||
{ id: 'order-detail', title: '订单详情', path: '/pages/mall/admin/order-management?action=detail' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'finance',
|
||||
title: '财务管理',
|
||||
icon: 'icon-finance',
|
||||
path: '/pages/mall/admin/finance-management',
|
||||
subMenus: [
|
||||
{ id: 'finance-overview', title: '财务概览', path: '/pages/mall/admin/finance-management' },
|
||||
{ id: 'withdrawals', title: '提现管理', path: '/pages/mall/admin/finance-management?tab=withdrawals' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'statistics',
|
||||
title: '用户统计',
|
||||
icon: 'icon-statistics',
|
||||
path: '/pages/mall/admin/user-statistics',
|
||||
subMenus: [
|
||||
{ id: 'user-stats', title: '用户统计', path: '/pages/mall/admin/user-statistics' },
|
||||
{ id: 'behavior', title: '用户行为', path: '/pages/mall/admin/user-statistics?tab=behavior' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'system',
|
||||
title: '系统设置',
|
||||
icon: 'icon-setting',
|
||||
path: '/pages/mall/admin/system-settings',
|
||||
subMenus: [
|
||||
{ id: 'basic', title: '基本设置', path: '/pages/mall/admin/system-settings' },
|
||||
{ id: 'security', title: '安全设置', path: '/pages/mall/admin/system-settings?tab=security' },
|
||||
{ id: 'email', title: '邮件设置', path: '/pages/mall/admin/system-settings?tab=email' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const activeMenuTitle = computed(() => {
|
||||
const menu = menuList.value.find(m => m.id === activeMenu.value)
|
||||
return menu ? menu.title : '管理后台'
|
||||
})
|
||||
|
||||
const activeSubMenus = computed(() => {
|
||||
const menu = menuList.value.find(m => m.id === activeMenu.value)
|
||||
return menu ? menu.subMenus || [] : []
|
||||
})
|
||||
|
||||
const currentPageTitle = computed(() => {
|
||||
const menu = menuList.value.find(m => m.id === props.currentPage)
|
||||
return menu ? menu.title : '管理后台'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
const handleMenuClick = (menu: any) => {
|
||||
activeMenu.value = menu.id
|
||||
activeSubMenu.value = menu.subMenus && menu.subMenus.length > 0 ? menu.subMenus[0].id : ''
|
||||
|
||||
// 添加或切换标签页
|
||||
const existingTab = tabs.value.find(tab => tab.id === menu.id)
|
||||
if (!existingTab) {
|
||||
tabs.value.push({
|
||||
id: menu.id,
|
||||
title: menu.title,
|
||||
closable: true
|
||||
})
|
||||
}
|
||||
activeTab.value = menu.id
|
||||
|
||||
// 只跳转到已在 pages.json 中声明的页面,不带 query 参数
|
||||
uni.navigateTo({
|
||||
url: menu.path
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubMenuClick = (subMenu: any) => {
|
||||
activeSubMenu.value = subMenu.id
|
||||
// ✅ 保留完整的路径(包括 query 参数),让页面内部通过 onLoad(options) 处理 tab 切换
|
||||
uni.navigateTo({
|
||||
url: subMenu.path
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
uni.showToast({
|
||||
title: '搜索功能',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleNotification = () => {
|
||||
uni.showToast({
|
||||
title: '通知中心',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleFullscreen = () => {
|
||||
uni.showToast({
|
||||
title: '全屏切换',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleUserMenu = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['个人信息', '退出登录'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
uni.showToast({
|
||||
title: '个人信息',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '退出登录',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const switchTab = (tabId: string) => {
|
||||
activeTab.value = tabId
|
||||
const menu = menuList.value.find(m => m.id === tabId)
|
||||
if (menu) {
|
||||
activeMenu.value = tabId
|
||||
uni.navigateTo({
|
||||
url: menu.path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const closeTab = (tabId: string) => {
|
||||
const index = tabs.value.findIndex(tab => tab.id === tabId)
|
||||
if (index > -1 && tabs.value[index].closable) {
|
||||
tabs.value.splice(index, 1)
|
||||
|
||||
// 如果关闭的是当前标签,切换到上一个
|
||||
if (activeTab.value === tabId && tabs.value.length > 0) {
|
||||
activeTab.value = tabs.value[Math.max(0, index - 1)].id
|
||||
const menu = menuList.value.find(m => m.id === activeTab.value)
|
||||
if (menu) {
|
||||
activeMenu.value = activeTab.value
|
||||
uni.navigateTo({
|
||||
url: menu.path
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ===== AdminLayout 样式 ===== */
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
/* ===== 左侧 Sider ===== */
|
||||
.admin-sider {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 240px;
|
||||
background-color: #001529;
|
||||
transition: width 0.3s ease;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-sider.sider-collapsed {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* Sider Header */
|
||||
.sider-header {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid #002140;
|
||||
background-color: #002140;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 一级菜单 (Icon Bar) */
|
||||
.menu-primary {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #002140;
|
||||
}
|
||||
|
||||
.menu-item-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 48px;
|
||||
margin: 8px 16px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.menu-item-primary:hover {
|
||||
background-color: #1890ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.menu-item-primary.active {
|
||||
background-color: #1890ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 二级菜单栏 */
|
||||
.menu-secondary {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #000c17;
|
||||
}
|
||||
|
||||
.secondary-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #002140;
|
||||
}
|
||||
|
||||
.secondary-title {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.secondary-content {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.sub-menu-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sub-menu-item.active {
|
||||
background-color: rgba(24, 144, 255, 0.2);
|
||||
color: #1890ff;
|
||||
border-right: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
.sub-menu-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
margin-left: 240px;
|
||||
transition: margin-left 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.admin-main.main-collapsed {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
/* ===== 顶部 Header ===== */
|
||||
.admin-header {
|
||||
height: 64px;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
font-size: 16px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #666666;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.header-action:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.notification-dot {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ffffff;
|
||||
}
|
||||
|
||||
.header-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.header-user:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* ===== 多标签页 Tab Bar ===== */
|
||||
.admin-tabs {
|
||||
height: 44px;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
background-color: #fafafa;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 2px solid #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
font-size: 14px;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
/* ===== 页面内容区 ===== */
|
||||
.page-content {
|
||||
flex: 1;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 24px;
|
||||
min-height: calc(100vh - 108px);
|
||||
}
|
||||
|
||||
/* ===== 响应式设计 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.admin-sider {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.admin-sider.sider-collapsed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
margin-left: 200px;
|
||||
}
|
||||
|
||||
.admin-main.main-collapsed {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 图标字体 ===== */
|
||||
.iconfont {
|
||||
font-family: 'iconfont';
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ===== 滚动条样式 ===== */
|
||||
.admin-sider ::-webkit-scrollbar,
|
||||
.page-content ::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.admin-sider ::-webkit-scrollbar-track,
|
||||
.page-content ::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.admin-sider ::-webkit-scrollbar-thumb,
|
||||
.page-content ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.admin-sider ::-webkit-scrollbar-thumb:hover,
|
||||
.page-content ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
250
layouts/admin/tags-view.uvue
Normal file
250
layouts/admin/tags-view.uvue
Normal file
@@ -0,0 +1,250 @@
|
||||
<!-- 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>
|
||||
20
layouts/admin/types.uts
Normal file
20
layouts/admin/types.uts
Normal file
@@ -0,0 +1,20 @@
|
||||
// 统一类型定义文件,避免重复定义冲突
|
||||
|
||||
export type MenuItem = {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
path?: string
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
export type UserInfo = {
|
||||
nickname: string
|
||||
role: string
|
||||
}
|
||||
|
||||
export type TagItem = {
|
||||
path: string
|
||||
title: string
|
||||
isAffix?: boolean
|
||||
}
|
||||
691
layouts/admin/utils/echarts-config.uts
Normal file
691
layouts/admin/utils/echarts-config.uts
Normal file
@@ -0,0 +1,691 @@
|
||||
// ECharts 配置工具 - CRMEB 风格图表配置
|
||||
// 订单统计图表配置(柱状图 + 折线图)
|
||||
export const getOrderChartOption = (period: string = '30days') => {
|
||||
const periods = {
|
||||
'30days': { label: '30天', days: 30 },
|
||||
'week': { label: '本周', days: 7 },
|
||||
'month': { label: '本月', days: 30 },
|
||||
'year': { label: '本年', days: 365 }
|
||||
}
|
||||
|
||||
const periodConfig = periods[period as keyof typeof periods] || periods['30days']
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: `订单统计 (${periodConfig.label})`,
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#262626'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: {
|
||||
color: '#999'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['订单金额', '订单数量'],
|
||||
top: 30,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666666'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: generateDateLabels(periodConfig.days),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单金额',
|
||||
position: 'left',
|
||||
axisLabel: {
|
||||
formatter: '¥{value}',
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '订单数量',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '订单金额',
|
||||
type: 'bar',
|
||||
data: generateAmountData(periodConfig.days),
|
||||
barWidth: '40%',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#36cfc9' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#40a9ff' },
|
||||
{ offset: 1, color: '#5cdbd3' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '订单数量',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: generateCountData(periodConfig.days),
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
color: '#52c41a',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: 'rgba(82, 196, 26, 0.1)' },
|
||||
{ offset: 1, color: 'rgba(82, 196, 26, 0.3)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
],
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicOut'
|
||||
}
|
||||
}
|
||||
|
||||
// 用户趋势图表配置
|
||||
export const getUserTrendOption = () => {
|
||||
return {
|
||||
title: {
|
||||
text: '用户增长趋势',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#262626'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['新增用户'],
|
||||
top: 30,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666666'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: generateDateLabels(30),
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '用户数量',
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
data: generateUserTrendData(30),
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: {
|
||||
color: '#1890ff',
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#1890ff',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: 'rgba(24, 144, 255, 0.1)' },
|
||||
{ offset: 1, color: 'rgba(24, 144, 255, 0.3)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
],
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicOut'
|
||||
}
|
||||
}
|
||||
|
||||
// 用户构成饼图配置
|
||||
export const getUserCompositionOption = () => {
|
||||
return {
|
||||
title: {
|
||||
text: '用户来源构成',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#262626'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c}% ({d}%)',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
top: 'center',
|
||||
itemGap: 16,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666666'
|
||||
},
|
||||
data: ['自然流量', '搜索引擎', '社交媒体', '广告投放', '其他']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户来源',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['60%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
formatter: '{b}\n{c}%'
|
||||
}
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 35,
|
||||
name: '自然流量',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#36cfc9' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 28,
|
||||
name: '搜索引擎',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#52c41a' },
|
||||
{ offset: 1, color: '#73d13d' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 20,
|
||||
name: '社交媒体',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#faad14' },
|
||||
{ offset: 1, color: '#ffc53d' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 12,
|
||||
name: '广告投放',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#f5222d' },
|
||||
{ offset: 1, color: '#ff7875' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
name: '其他',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{ offset: 0, color: '#722ed1' },
|
||||
{ offset: 1, color: '#b37feb' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicOut'
|
||||
}
|
||||
}
|
||||
|
||||
// 用户统计多折线图配置
|
||||
export const getUserStatisticsOption = () => {
|
||||
return {
|
||||
title: {
|
||||
text: '用户数据趋势分析',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#262626'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
borderColor: 'transparent',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 12
|
||||
},
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
crossStyle: {
|
||||
color: '#999'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['新增用户', '访客数', '浏览量', '成交用户', '付费会员'],
|
||||
top: 30,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#666666'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '10%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: generateDateLabels(30, 7), // 30天的数据,每7天一个标签
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#e8e8e8'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
axisLabel: {
|
||||
color: '#999999',
|
||||
fontSize: 12
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#f0f0f0',
|
||||
type: 'dashed'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
data: generateUserStatisticsData('newUsers', 7),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
color: '#1890ff',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#1890ff',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '访客数',
|
||||
type: 'line',
|
||||
data: generateUserStatisticsData('visitors', 7),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
color: '#52c41a',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '浏览量',
|
||||
type: 'line',
|
||||
data: generateUserStatisticsData('pageViews', 7),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
color: '#faad14',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#faad14',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '成交用户',
|
||||
type: 'line',
|
||||
data: generateUserStatisticsData('conversions', 7),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
color: '#f5222d',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#f5222d',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true
|
||||
},
|
||||
{
|
||||
name: '付费会员',
|
||||
type: 'line',
|
||||
data: generateUserStatisticsData('vipUsers', 7),
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: {
|
||||
color: '#722ed1',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#722ed1',
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 2
|
||||
},
|
||||
smooth: true
|
||||
}
|
||||
],
|
||||
animationDuration: 1000,
|
||||
animationEasing: 'cubicOut'
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:生成日期标签
|
||||
function generateDateLabels(days: number, step: number = 1): string[] {
|
||||
const labels = []
|
||||
const today = new Date()
|
||||
|
||||
for (let i = days - 1; i >= 0; i -= step) {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() - i)
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
labels.push(`${month}-${day}`)
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
// 辅助函数:生成订单金额数据
|
||||
function generateAmountData(days: number): number[] {
|
||||
const data = []
|
||||
for (let i = 0; i < days; i++) {
|
||||
// 生成12000-25000之间的随机金额
|
||||
data.push(Math.floor(Math.random() * 13000) + 12000)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 辅助函数:生成订单数量数据
|
||||
function generateCountData(days: number): number[] {
|
||||
const data = []
|
||||
for (let i = 0; i < days; i++) {
|
||||
// 生成50-150之间的随机数量
|
||||
data.push(Math.floor(Math.random() * 100) + 50)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// 辅助函数:生成用户趋势数据
|
||||
function generateUserTrendData(days: number): number[] {
|
||||
const data = []
|
||||
let base = 100
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
base += Math.floor(Math.random() * 20) - 5 // -5到+15的随机变化
|
||||
base = Math.max(50, base) // 最低50
|
||||
data.push(base)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// 辅助函数:生成用户统计数据
|
||||
function generateUserStatisticsData(type: string, points: number): number[] {
|
||||
const data = []
|
||||
const baseValues = {
|
||||
newUsers: 120,
|
||||
visitors: 450,
|
||||
pageViews: 680,
|
||||
conversions: 45,
|
||||
vipUsers: 12
|
||||
}
|
||||
|
||||
let base = baseValues[type as keyof typeof baseValues] || 100
|
||||
|
||||
for (let i = 0; i < points; i++) {
|
||||
const variation = type === 'vipUsers' ? 0.3 : 0.2 // 付费会员变化小一些
|
||||
base += Math.floor(Math.random() * (base * variation * 2)) - (base * variation)
|
||||
base = Math.max(0, base)
|
||||
data.push(Math.floor(base))
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Mock API 数据接口
|
||||
export const mockApi = {
|
||||
// 获取订单统计数据
|
||||
getOrderStats: (period: string = '30days') => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
period,
|
||||
amountData: generateAmountData(30),
|
||||
countData: generateCountData(30),
|
||||
dateLabels: generateDateLabels(30)
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户趋势数据
|
||||
getUserTrend: () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
data: generateUserTrendData(30),
|
||||
dateLabels: generateDateLabels(30)
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户构成数据
|
||||
getUserComposition: () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{ name: '自然流量', value: 35, color: '#1890ff' },
|
||||
{ name: '搜索引擎', value: 28, color: '#52c41a' },
|
||||
{ name: '社交媒体', value: 20, color: '#faad14' },
|
||||
{ name: '广告投放', value: 12, color: '#f5222d' },
|
||||
{ name: '其他', value: 5, color: '#722ed1' }
|
||||
])
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
// 获取用户统计数据
|
||||
getUserStatistics: () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
newUsers: generateUserStatisticsData('newUsers', 7),
|
||||
visitors: generateUserStatisticsData('visitors', 7),
|
||||
pageViews: generateUserStatisticsData('pageViews', 7),
|
||||
conversions: generateUserStatisticsData('conversions', 7),
|
||||
vipUsers: generateUserStatisticsData('vipUsers', 7),
|
||||
dateLabels: generateDateLabels(30, 7)
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出所有配置
|
||||
export const chartConfigs = {
|
||||
orderChart: getOrderChartOption,
|
||||
userTrendChart: getUserTrendOption,
|
||||
userCompositionChart: getUserCompositionOption,
|
||||
userStatisticsChart: getUserStatisticsOption,
|
||||
mockApi
|
||||
}
|
||||
Reference in New Issue
Block a user