初步构建起页面布局

This commit is contained in:
2026-01-23 17:55:26 +08:00
parent 2b0ee0c8b6
commit b6ad549737
37 changed files with 13405 additions and 5385 deletions

272
ADMIN_LAYOUT_GUIDE.md Normal file
View File

@@ -0,0 +1,272 @@
# Mall Admin 布局系统使用指南
## 概述
本项目已基于CRMEB Admin的vertical布局设计创建了一套统一的admin管理后台布局系统。该系统提供
- 🎨 **统一视觉设计** - 参考CRMEB Admin的深色侧边栏风格
- 📱 **响应式布局** - 支持桌面端和移动端自适应
- 🔧 **灵活配置** - 支持菜单折叠、主题切换等功能
- 🧭 **智能导航** - 自动高亮当前页面,支持子菜单展开
## 文件结构
```
layouts/
├── admin/
│ └── index.uvue # 主布局组件
pages/mall/admin/
├── index.uvue # 首页(已集成布局)
├── user-management.uvue # 用户管理(已集成布局)
└── ... # 其他页面
pages.json # 页面配置(已更新)
```
## 快速开始
### 1. 在页面中使用AdminLayout
```vue
<template>
<AdminLayout current-page="your-page-id">
<!-- 你的页面内容 -->
<view class="your-page-content">
<!-- 页面具体内容 -->
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import AdminLayout from '@/layouts/admin/index.uvue'
// 你的页面逻辑
</script>
```
### 2. current-page 参数说明
`current-page` 属性用于标识当前页面,对应的菜单项会被高亮显示:
| 页面 | current-page 值 | 说明 |
|------|----------------|------|
| 首页 | `dashboard` | 主页 |
| 用户管理 | `user-list` | 用户列表页 |
| 商品管理 | `product-list` | 商品列表页 |
| 订单管理 | `order` | 订单管理页 |
| 商家管理 | `merchant-list` | 商家列表页 |
| 系统设置 | `system` | 系统设置页 |
### 3. 页面配置
`pages.json`所有admin页面都需要设置
```json
{
"path": "admin/your-page",
"style": {
"navigationBarTitleText": "页面标题",
"navigationStyle": "custom"
}
}
```
**注意**: `navigationStyle: "custom"` 是必需的用于隐藏uni-app默认导航栏。
## AdminLayout 组件功能
### 侧边栏功能
#### 菜单结构
```javascript
menuList: [
{
id: 'dashboard', // 菜单唯一标识
title: '首页', // 菜单显示文本
icon: 'icon-shouye', // 图标类名
path: '/pages/mall/admin/index' // 跳转路径
},
{
id: 'user',
title: '用户管理',
icon: 'icon-yonghuguanli',
children: [ // 子菜单
{
id: 'user-list',
title: '用户列表',
path: '/pages/mall/admin/user-management'
}
]
}
]
```
#### 菜单图标
系统使用iconfont图标库支持以下图标
- `icon-shouye` - 首页
- `icon-yonghuguanli` - 用户管理
- `icon-shangpinguanli` - 商品管理
- `icon-dingdanguanli` - 订单管理
- `icon-caiwuguanli` - 财务管理
- `icon-yingxiaoguanli` - 营销管理
- `icon-xitongshezhi` - 系统设置
- `icon-shangjiaguanli` - 商家管理
### 顶部导航栏
#### 左侧功能
- **菜单切换按钮** - 展开/收起侧边栏
- **面包屑导航** - 显示当前页面标题
#### 右侧功能
- **通知中心** - 显示未读消息数量
- **用户头像** - 点击进入个人资料
### 响应式设计
#### 桌面端 (> 768px)
- 侧边栏默认展开宽度240rpx
- 支持折叠到80rpx
- 完整显示菜单文本和图标
#### 平板端 (600px - 768px)
- 侧边栏可折叠
- 菜单文本正常显示
#### 移动端 (< 600px)
- 侧边栏默认隐藏
- 点击菜单按钮显示侧边栏
- 菜单文本正常显示
- 点击遮罩层关闭侧边栏
## 样式定制
### 主题色配置
系统默认使用以下颜色:
```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);
```
### 自定义样式
如需修改样式,可以在 `layouts/admin/index.uvue``<style>` 部分进行调整:
```scss
// 修改侧边栏背景色
.admin-sidebar {
background-color: #002140; // 更深的蓝色
}
// 修改菜单项激活状态
.menu-link-active {
background-color: #0050b3; // 不同的激活色
}
```
## 数据持久化
系统自动保存用户偏好设置到本地存储:
- **侧边栏折叠状态** - `admin_sidebar_collapsed`
- **展开的子菜单** - `admin_open_menus`
- **用户信息** - 从本地存储读取
## 扩展指南
### 添加新菜单项
1.`menuList` 中添加新菜单配置
2. 为菜单项分配唯一的 `id`
3. 设置合适的图标和路径
4. 如果有子菜单,使用 `children` 数组
### 添加新页面
1. 创建页面文件 `pages/mall/admin/new-page.uvue`
2. 使用 `AdminLayout` 包装页面内容
3. 设置正确的 `current-page` 属性
4.`pages.json` 中注册页面,设置 `"navigationStyle": "custom"`
### 自定义菜单图标
1. 在iconfont中添加新图标
2. 获取图标的class名称`icon-new-feature`
3. 在菜单配置中使用该class名
## 故障排除
### 菜单不显示
- 检查页面是否正确导入了 `AdminLayout` 组件
- 确认 `current-page` 属性值与菜单配置中的 `id` 匹配
### 样式异常
- 检查页面是否设置了 `"navigationStyle": "custom"`
- 确认没有与uni-app默认样式冲突
### 移动端适配问题
- 检查响应式断点是否正确设置
- 确认遮罩层和侧边栏切换逻辑正常
## 迁移现有页面
对于已有的admin页面按以下步骤迁移
1. **导入AdminLayout组件**
```javascript
import AdminLayout from '@/layouts/admin/index.uvue'
```
2. **包装页面内容**
```vue
<template>
<AdminLayout current-page="page-id">
<!-- 原有页面内容 -->
</AdminLayout>
</template>
```
3. **更新pages.json配置**
```json
{
"style": {
"navigationStyle": "custom"
}
}
```
4. **移除原有导航栏代码**
- 删除页面内的自定义导航栏
- 移除相关的导航栏样式
## 最佳实践
1. **保持菜单结构清晰** - 使用合理的分组和层级
2. **图标选择一致** - 使用统一的图标风格
3. **页面标题准确** - 确保面包屑显示正确的页面标题
4. **响应式测试** - 在不同设备上测试布局效果
5. **性能优化** - 避免在菜单中放置大量数据
## 更新日志
### v1.0.0 (2025-01-23)
- ✅ 基于CRMEB Admin设计创建统一布局系统
- ✅ 支持响应式布局和菜单折叠
- ✅ 集成用户管理和首页
- ✅ 完成pages.json配置更新
- ✅ 提供完整的使用文档
---
如有问题或需要进一步定制,请参考源代码或联系开发团队。

429
App.uvue
View File

@@ -1,54 +1,413 @@
<template>
<view class="app-root">
<view class="dev-probe">APP 已挂载dev probe</view>
<view class="dev-nav">
<a class="dev-nav-btn" href="#/pages/user/boot">去启动页 boot</a>
<a class="dev-nav-btn" href="#/pages/user/login">去登录 login</a>
<a class="dev-nav-btn" href="#/pages/mall/consumer/index">去商城首页</a>
</view>
<slot />
</view>
</template>
<script setup lang="uts">
// 简化的App组件
</script>
<style>
/* ===== 全局重置样式 ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: #262626;
background-color: #f0f2f5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
view, text, input, textarea, button {
box-sizing: border-box;
}
/* ===== 全局主题色 ===== */
:root {
/* 主色调 */
--primary-color: #1890ff;
--primary-hover: #40a9ff;
--primary-active: #096dd9;
/* 成功色 */
--success-color: #52c41a;
--success-hover: #73d13d;
--success-active: #389e0d;
/* 警告色 */
--warning-color: #faad14;
--warning-hover: #ffc53d;
--warning-active: #d48806;
/* 错误色 */
--error-color: #ff4d4f;
--error-hover: #ff7875;
--error-active: #d4380d;
/* 中性色 */
--text-primary: #262626;
--text-secondary: #595959;
--text-disabled: #bfbfbf;
--text-inverse: #ffffff;
/* 边框色 */
--border-color: #d9d9d9;
--border-color-light: #f0f0f0;
--border-color-dark: #bfbfbf;
/* 背景色 */
--background-color: #ffffff;
--background-color-light: #fafafa;
--background-color-dark: #f5f5f5;
/* 阴影 */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-xl: 0 6px 16px rgba(0, 0, 0, 0.12);
/* 圆角 */
--border-radius-sm: 2px;
--border-radius: 4px;
--border-radius-lg: 6px;
--border-radius-xl: 8px;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-xxl: 48px;
/* 字体大小 */
--font-size-xs: 12px;
--font-size-sm: 14px;
--font-size: 16px;
--font-size-lg: 18px;
--font-size-xl: 20px;
--font-size-xxl: 24px;
/* 行高 */
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
}
/* ===== 全局字体设置 ===== */
.text-xs { font-size: var(--font-size-xs); }
.text-sm { font-size: var(--font-size-sm); }
.text-base { font-size: var(--font-size); }
.text-lg { font-size: var(--font-size-lg); }
.text-xl { font-size: var(--font-size-xl); }
.text-2xl { font-size: var(--font-size-xxl); }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
/* ===== 全局颜色类 ===== */
.text-primary { color: var(--primary-color); }
.text-success { color: var(--success-color); }
.text-warning { color: var(--warning-color); }
.text-error { color: var(--error-color); }
.text-secondary { color: var(--text-secondary); }
.text-disabled { color: var(--text-disabled); }
/* ===== 全局背景类 ===== */
.bg-white { background-color: var(--background-color); }
.bg-light { background-color: var(--background-color-light); }
.bg-dark { background-color: var(--background-color-dark); }
/* ===== 全局边框类 ===== */
.border { border: 1px solid var(--border-color); }
.border-light { border: 1px solid var(--border-color-light); }
.border-primary { border: 1px solid var(--primary-color); }
/* ===== 全局阴影类 ===== */
.shadow-sm { box-shadow: var(--shadow-sm); }
.shadow { box-shadow: var(--shadow); }
.shadow-lg { box-shadow: var(--shadow-lg); }
.shadow-xl { box-shadow: var(--shadow-xl); }
/* ===== 全局圆角类 ===== */
.rounded-sm { border-radius: var(--border-radius-sm); }
.rounded { border-radius: var(--border-radius); }
.rounded-lg { border-radius: var(--border-radius-lg); }
.rounded-xl { border-radius: var(--border-radius-xl); }
/* ===== 全局间距类 ===== */
.m-1 { margin: var(--spacing-xs); }
.m-2 { margin: var(--spacing-sm); }
.m-3 { margin: var(--spacing); }
.m-4 { margin: var(--spacing-lg); }
.m-5 { margin: var(--spacing-xl); }
.p-1 { padding: var(--spacing-xs); }
.p-2 { padding: var(--spacing-sm); }
.p-3 { padding: var(--spacing); }
.p-4 { padding: var(--spacing-lg); }
.p-5 { padding: var(--spacing-xl); }
/* ===== 全局布局类 ===== */
.flex { display: flex; }
.inline-flex { display: inline-flex; }
.block { display: block; }
.inline-block { display: inline-block; }
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
.items-stretch { align-items: stretch; }
.justify-start { justify-content: flex-start; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
.flex-1 { flex: 1; }
.flex-auto { flex: auto; }
.flex-none { flex: none; }
/* ===== 全局工具类 ===== */
.w-full { width: 100%; }
.h-full { height: 100%; }
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.cursor-pointer { cursor: pointer; }
.cursor-default { cursor: default; }
/* ===== 响应式断点 ===== */
@media (min-width: 1200px) {
:root {
--container-width: 1200px;
}
}
@media (max-width: 1199px) and (min-width: 768px) {
:root {
--container-width: 100%;
}
}
@media (max-width: 767px) {
:root {
--container-width: 100%;
}
}
/* ===== App根容器 ===== */
.app-root {
min-height: 100vh;
background-color: var(--background-color);
}
.dev-probe {
position: fixed;
left: 16rpx;
top: 16rpx;
z-index: 999999;
padding: 10rpx 14rpx;
border-radius: 12rpx;
background: rgba(17, 24, 39, 0.82);
color: #fff;
font-size: 22rpx;
line-height: 1.2;
}
.dev-nav {
position: fixed;
left: 16rpx;
top: 64rpx;
z-index: 999999;
/* ===== 24栅格系统 ===== */
.row {
display: flex;
flex-direction: column;
gap: 10rpx;
flex-wrap: wrap;
margin: 0 calc(-1 * var(--spacing-sm));
}
.dev-nav-btn {
display: inline-block;
padding: 10rpx 14rpx;
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.92);
color: #111827;
font-size: 22rpx;
border: 2rpx solid rgba(17, 24, 39, 0.12);
text-decoration: none;
.col {
padding: 0 var(--spacing-sm);
}
.col-1 { flex: 0 0 4.16666667%; max-width: 4.16666667%; }
.col-2 { flex: 0 0 8.33333333%; max-width: 8.33333333%; }
.col-3 { flex: 0 0 12.5%; max-width: 12.5%; }
.col-4 { flex: 0 0 16.66666667%; max-width: 16.66666667%; }
.col-5 { flex: 0 0 20.83333333%; max-width: 20.83333333%; }
.col-6 { flex: 0 0 25%; max-width: 25%; }
.col-7 { flex: 0 0 29.16666667%; max-width: 29.16666667%; }
.col-8 { flex: 0 0 33.33333333%; max-width: 33.33333333%; }
.col-9 { flex: 0 0 37.5%; max-width: 37.5%; }
.col-10 { flex: 0 0 41.66666667%; max-width: 41.66666667%; }
.col-11 { flex: 0 0 45.83333333%; max-width: 45.83333333%; }
.col-12 { flex: 0 0 50%; max-width: 50%; }
.col-13 { flex: 0 0 54.16666667%; max-width: 54.16666667%; }
.col-14 { flex: 0 0 58.33333333%; max-width: 58.33333333%; }
.col-15 { flex: 0 0 62.5%; max-width: 62.5%; }
.col-16 { flex: 0 0 66.66666667%; max-width: 66.66666667%; }
.col-17 { flex: 0 0 70.83333333%; max-width: 70.83333333%; }
.col-18 { flex: 0 0 75%; max-width: 75%; }
.col-19 { flex: 0 0 79.16666667%; max-width: 79.16666667%; }
.col-20 { flex: 0 0 83.33333333%; max-width: 83.33333333%; }
.col-21 { flex: 0 0 87.5%; max-width: 87.5%; }
.col-22 { flex: 0 0 91.66666667%; max-width: 91.66666667%; }
.col-23 { flex: 0 0 95.83333333%; max-width: 95.83333333%; }
.col-24 { flex: 0 0 100%; max-width: 100%; }
/* ===== 响应式栅格 ===== */
@media (max-width: 1199px) {
.col-lg-1 { flex: 0 0 4.16666667%; max-width: 4.16666667%; }
.col-lg-2 { flex: 0 0 8.33333333%; max-width: 8.33333333%; }
.col-lg-3 { flex: 0 0 12.5%; max-width: 12.5%; }
.col-lg-4 { flex: 0 0 16.66666667%; max-width: 16.66666667%; }
.col-lg-5 { flex: 0 0 20.83333333%; max-width: 20.83333333%; }
.col-lg-6 { flex: 0 0 25%; max-width: 25%; }
.col-lg-7 { flex: 0 0 29.16666667%; max-width: 29.16666667%; }
.col-lg-8 { flex: 0 0 33.33333333%; max-width: 33.33333333%; }
.col-lg-9 { flex: 0 0 37.5%; max-width: 37.5%; }
.col-lg-10 { flex: 0 0 41.66666667%; max-width: 41.66666667%; }
.col-lg-11 { flex: 0 0 45.83333333%; max-width: 45.83333333%; }
.col-lg-12 { flex: 0 0 50%; max-width: 50%; }
.col-lg-13 { flex: 0 0 54.16666667%; max-width: 54.16666667%; }
.col-lg-14 { flex: 0 0 58.33333333%; max-width: 58.33333333%; }
.col-lg-15 { flex: 0 0 62.5%; max-width: 62.5%; }
.col-lg-16 { flex: 0 0 66.66666667%; max-width: 66.66666667%; }
.col-lg-17 { flex: 0 0 70.83333333%; max-width: 70.83333333%; }
.col-lg-18 { flex: 0 0 75%; max-width: 75%; }
.col-lg-19 { flex: 0 0 79.16666667%; max-width: 79.16666667%; }
.col-lg-20 { flex: 0 0 83.33333333%; max-width: 83.33333333%; }
.col-lg-21 { flex: 0 0 87.5%; max-width: 87.5%; }
.col-lg-22 { flex: 0 0 91.66666667%; max-width: 91.66666667%; }
.col-lg-23 { flex: 0 0 95.83333333%; max-width: 95.83333333%; }
.col-lg-24 { flex: 0 0 100%; max-width: 100%; }
}
@media (max-width: 767px) {
.col-md-1 { flex: 0 0 4.16666667%; max-width: 4.16666667%; }
.col-md-2 { flex: 0 0 8.33333333%; max-width: 8.33333333%; }
.col-md-3 { flex: 0 0 12.5%; max-width: 12.5%; }
.col-md-4 { flex: 0 0 16.66666667%; max-width: 16.66666667%; }
.col-md-5 { flex: 0 0 20.83333333%; max-width: 20.83333333%; }
.col-md-6 { flex: 0 0 25%; max-width: 25%; }
.col-md-7 { flex: 0 0 29.16666667%; max-width: 29.16666667%; }
.col-md-8 { flex: 0 0 33.33333333%; max-width: 33.33333333%; }
.col-md-9 { flex: 0 0 37.5%; max-width: 37.5%; }
.col-md-10 { flex: 0 0 41.66666667%; max-width: 41.66666667%; }
.col-md-11 { flex: 0 0 45.83333333%; max-width: 45.83333333%; }
.col-md-12 { flex: 0 0 50%; max-width: 50%; }
.col-md-13 { flex: 0 0 54.16666667%; max-width: 54.16666667%; }
.col-md-14 { flex: 0 0 58.33333333%; max-width: 58.33333333%; }
.col-md-15 { flex: 0 0 62.5%; max-width: 62.5%; }
.col-md-16 { flex: 0 0 66.66666667%; max-width: 66.66666667%; }
.col-md-17 { flex: 0 0 70.83333333%; max-width: 70.83333333%; }
.col-md-18 { flex: 0 0 75%; max-width: 75%; }
.col-md-19 { flex: 0 0 79.16666667%; max-width: 79.16666667%; }
.col-md-20 { flex: 0 0 83.33333333%; max-width: 83.33333333%; }
.col-md-21 { flex: 0 0 87.5%; max-width: 87.5%; }
.col-md-22 { flex: 0 0 91.66666667%; max-width: 91.66666667%; }
.col-md-23 { flex: 0 0 95.83333333%; max-width: 95.83333333%; }
.col-md-24 { flex: 0 0 100%; max-width: 100%; }
.row {
margin: 0 calc(-1 * var(--spacing-xs));
}
.col {
padding: 0 var(--spacing-xs);
}
}
/* ===== 按钮基础样式 ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-sm) var(--spacing);
font-size: var(--font-size-sm);
font-weight: 400;
line-height: var(--line-height-tight);
white-space: nowrap;
border: 1px solid transparent;
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
color: var(--text-inverse);
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-secondary {
color: var(--text-secondary);
background-color: var(--background-color);
border-color: var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
color: var(--primary-color);
border-color: var(--primary-color);
}
/* ===== 卡片基础样式 ===== */
.card {
background-color: var(--background-color);
border: 1px solid var(--border-color-light);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
}
/* ===== 输入框基础样式 ===== */
.input {
display: block;
width: 100%;
padding: var(--spacing-sm) var(--spacing);
font-size: var(--font-size-sm);
line-height: var(--line-height-tight);
color: var(--text-primary);
background-color: var(--background-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
transition: border-color 0.2s ease;
}
.input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* ===== 滚动条样式 ===== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--background-color-light);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-disabled);
}
</style>

570
CRMEB_DASHBOARD_README.md Normal file
View File

@@ -0,0 +1,570 @@
# CRMEB 标准版后台管理系统
## 📋 项目概述
基于 uni-app-x 完全自主开发的 CRMEB 风格后台管理系统,严格遵循 CRMEB 设计规范,实现完整的数据看板和用户统计功能。
## 🏗️ 目录结构
```
mall/
├── App.uvue # 全局样式配置
├── layouts/
│ └── admin/
│ ├── index.uvue # 主布局组件
│ ├── components/
│ │ └── card.uvue # 卡片组件
│ └── utils/
│ └── echarts-config.uts # ECharts配置
├── pages/
│ ├── minimal.uvue # 测试页面
│ └── mall/
│ └── admin/
│ ├── index.uvue # 数据看板
│ ├── user-management.uvue # 用户管理
│ ├── product-management.uvue # 商品管理
│ ├── order-management.uvue # 订单管理
│ ├── finance-management.uvue # 财务管理
│ └── user-statistics.uvue # 用户统计页
├── pages.json # 页面配置
└── CRMEB_DASHBOARD_README.md # 项目文档
```
## 🎨 设计规范
### 全局样式体系
- **24栅格系统**: 响应式布局支持1-24列
- **CSS变量**: 统一的颜色、间距、圆角规范
- **全局重置**: 消除浏览器默认样式差异
- **主题色**: CRMEB 风格的蓝色系配色
### 布局架构
- **AdminLayout**: 左侧菜单 + 顶部导航 + 标签页 + 内容区
- **垂直菜单**: 一级图标菜单 + 二级文字菜单 + 折叠功能
- **标签页**: 可关闭的多标签页,支持切换导航
- **内容区**: flex:1 + height:0 + scroll-view 确保正确滚动
## 📊 核心功能
### 1. 数据看板 (Dashboard)
#### KPI 指标卡片 (第一行)
```vue
<!-- 4个KPI卡片销售额/访问量/订单量/新增用户 -->
<view class="kpi-cards-row">
<view class="kpi-card">
<view class="card-content">
<view class="card-header">
<text class="card-title">销售额</text>
<view class="card-tag">今日</view>
</view>
<view class="card-value">
<text class="value-number">¥125,680.50</text>
<view class="value-trend up">+5.7%</view>
</view>
<view class="card-footer">
<text>昨日¥118,920.30</text>
<text>本月累计¥2,857,808.90</text>
</view>
</view>
</view>
</view>
```
#### 订单统计图表 (第二行)
```vue
<!-- 柱状图(订单金额) + 折线图(订单数量) -->
<AdminCard title="订单统计">
<view class="chart-container">
<!-- ECharts 组合图表 -->
</view>
</AdminCard>
```
#### 用户分析图表 (第三行)
```vue
<!-- 用户趋势折线图 + 用户构成饼图 -->
<view class="charts-row two-cols">
<AdminCard title="用户趋势"><!-- 折线图 --></AdminCard>
<AdminCard title="用户构成"><!-- 饼图 --></AdminCard>
</view>
```
### 2. 用户统计页
#### 筛选条件栏
```vue
<view class="filter-section">
<view class="filter-row">
<view class="filter-left">
<!-- 用户渠道选择器 + 日期范围选择器 -->
</view>
<view class="filter-right">
<!-- 查询按钮 + 导出按钮 -->
</view>
</view>
</view>
```
#### 指标概览 (6个KPI卡片)
```vue
<view class="metrics-row">
<!-- 累计用户/访客数/浏览量/新增用户/成交用户/付费会员 -->
</view>
```
#### 多折线趋势图
```vue
<view class="chart-section">
<view class="admin-card">
<!-- 图表图例 + 多折线图表 -->
</view>
</view>
```
## 🔧 技术实现
### AdminLayout 组件
#### 核心特性
```javascript
// 双层侧边栏
const menuList = ref([
{
id: 'dashboard',
title: '首页',
icon: 'icon-dashboard',
path: '/pages/mall/admin/index',
subMenus: [] // 二级菜单
}
// ... 其他菜单项
])
// 标签页管理
const tabs = ref([
{ id: 'dashboard', title: '首页', closable: false }
])
// 折叠状态
const isCollapsed = ref(false)
```
#### 布局结构
```vue
<view class="admin-layout">
<!-- 左侧 Sider -->
<view class="admin-sider" :class="{ 'sider-collapsed': isCollapsed }">
<view class="sider-header"><!-- Logo + 折叠按钮 --></view>
<view class="menu-primary"><!-- 一级菜单 --></view>
<view class="menu-secondary"><!-- 二级菜单 --></view>
</view>
<!-- 主内容区 -->
<view class="admin-main">
<view class="admin-header"><!-- 顶部导航 --></view>
<view class="admin-tabs"><!-- 标签页 --></view>
<scroll-view class="page-content" scroll-y="true" :style="{ flex: '1', height: '0' }">
<slot /><!-- 页面内容 -->
</scroll-view>
</view>
</view>
```
### ECharts 图表配置
#### 组合图表配置
```javascript
export const getOrderChartOption = (period) => ({
series: [
{
name: '订单金额',
type: 'bar',
data: amountData,
itemStyle: { color: '#1890ff' }
},
{
name: '订单数量',
type: 'line',
data: countData,
itemStyle: { color: '#52c41a' }
}
]
})
```
#### 多折线图配置
```javascript
export const getUserStatisticsOption = () => ({
series: [
{ name: '新增用户', type: 'line', data: newUsersData },
{ name: '访客数', type: 'line', data: visitorsData },
// ... 更多数据线
]
})
```
## 📱 响应式设计
### 断点系统
```scss
/* >=1200px: 4卡片一行 */
.kpi-cards-row { display: flex; gap: 24px; }
/* <=1200px: 2卡片一行 */
@media (max-width: 1199px) {
.kpi-card { min-width: 45%; }
}
/* <=768px: 单列布局 */
@media (max-width: 767px) {
.kpi-cards-row { flex-direction: column; }
.charts-row.two-cols { flex-direction: column; }
}
```
### 栅格系统
```scss
/* 24列栅格系统 */
.col-6 { flex: 0 0 25%; max-width: 25%; }
.col-12 { flex: 0 0 50%; max-width: 50%; }
.col-24 { flex: 0 0 100%; max-width: 100%; }
```
## 🚀 运行指南
### 开发环境
```bash
# HBuilderX 中运行
# 选择:运行 -> 运行到浏览器 -> Chrome
```
### 页面访问
- **数据看板**: `/pages/mall/admin/index`
- **用户统计**: `/pages/mall/admin/user-statistics`
- **其他页面**: 通过左侧菜单导航
### 功能测试
1. **菜单导航**: 点击左侧菜单切换页面
2. **标签页**: 点击标签切换,点击关闭按钮关闭
3. **折叠功能**: 点击折叠按钮收起/展开菜单
4. **图表展示**: 查看各种数据图表
5. **响应式**: 调整浏览器窗口测试适配
## 📚 开发规范
### 文件命名
- **组件**: PascalCase (`AdminLayout.vue`)
- **页面**: kebab-case (`user-statistics.uvue`)
- **工具**: camelCase (`echarts-config.uts`)
### 代码组织
```vue
<template>
<!-- 模板清晰的结构层次 -->
</template>
<script setup lang="uts">
// 导入语句
// 响应式数据
// 计算属性
// 生命周期
// 方法定义
</script>
<style>
/* 组件内样式避免scoped污染 */
</style>
```
### 样式原则
- **组件内样式**: 避免 `scoped`,确保样式隔离
- **CSS变量**: 使用统一的主题变量
- **BEM命名**: 清晰的样式命名规范
- **移动优先**: 响应式设计从移动端开始
## 🎯 项目特色
### ✅ 完全自主开发
- **0%源码复制**: 100%自主编写
- **CRMEB风格**: 严格遵循设计规范
- **技术先进**: Vue 3 + TypeScript + uni-app-x
- **功能完整**: 数据看板 + 用户统计双页面
### ✅ 设计还原度高
- **布局结构**: 1:1还原CRMEB后台布局
- **视觉风格**: 白底轻阴影Element-UI设计语言
- **交互体验**: 流畅的动画和反馈效果
- **响应式**: 全设备适配
### ✅ 架构优秀
- **组件化**: 模块化组件设计
- **可扩展**: 易于添加新功能
- **可维护**: 清晰的代码结构
- **性能优化**: 合理的渲染策略
## 📋 功能清单
### 已实现功能
- ✅ CRMEB风格垂直菜单布局
- ✅ 顶部多标签页系统
- ✅ 双层侧边栏导航
- ✅ KPI指标卡片展示
- ✅ 订单统计组合图表
- ✅ 用户趋势分析图表
- ✅ 用户构成饼图
- ✅ 用户统计筛选功能
- ✅ 多折线趋势图表
- ✅ 响应式24栅格布局
- ✅ 完整的样式系统
- ✅ ECharts图表配置
### 扩展功能
- 🔄 ECharts实际集成
- 🔄 数据实时更新
- 🔄 图表交互功能
- 🔄 数据导出功能
- 🔄 更多管理页面
---
## 🎉 总结
本项目成功实现了CRMEB标准版后台管理系统具备完整的数据看板和用户统计功能。通过严格遵循CRMEB的设计规范和自主开发确保了代码质量和技术先进性。
项目采用了现代化的技术栈,实现了响应式设计和模块化架构,为后续功能扩展奠定了坚实基础。
---
## 🚀 部署运行
### 开发环境
```bash
# HBuilderX 中运行
# 选择:运行 -> 运行到浏览器
```
### 访问页面
- **数据看板**: `/pages/mall/admin/index`
- **用户统计**: `/pages/mall/admin/user-statistics`
- **其他页面**: 通过左侧菜单导航
### 功能验证
1. **菜单导航**: 左侧双层菜单切换页面
2. **标签页**: 顶部标签页切换和关闭
3. **折叠功能**: 菜单栏收起/展开
4. **图表展示**: 查看各种数据可视化
5. **响应式**: 调整窗口测试适配效果
## 📋 功能清单
### ✅ 已实现功能
- [x] CRMEB风格垂直菜单布局
- [x] 顶部多标签页系统
- [x] 双层侧边栏导航
- [x] 二级菜单Tab切换功能
- [x] KPI指标卡片展示
- [x] 订单统计组合图表
- [x] 用户趋势分析图表
- [x] 用户构成饼图
- [x] 用户统计筛选功能
- [x] 多折线趋势图表
- [x] 响应式24栅格布局
- [x] 完整的样式系统
- [x] ECharts图表配置
- [x] 页面参数处理(onLoad)
- [x] Tab内部状态管理
### 🎯 技术亮点
- **完全自主开发**: 0%源码复制100%原创
- **CRMEB风格还原**: 严格遵循设计规范
- **现代技术栈**: Vue 3 + TypeScript + uni-app-x
- **架构设计**: 模块化组件,易于维护
- **用户体验**: 流畅交互,响应式适配
---
## 🔧 二级菜单Tab切换机制详解
### 实现原理
CRMEB后台的二级菜单采用 **页面级Tab切换** 模式:
- 点击一级菜单:跳转到对应页面的**默认Tab**
- 点击二级菜单:跳转到同一页面的**指定Tab**
- 通过URL参数控制Tab状态
### 技术实现
#### 1. AdminLayout菜单配置
```javascript
const menuList = ref([
{
id: 'user',
title: '用户管理',
icon: 'icon-user',
path: '/pages/mall/admin/user-management',
subMenus: [
{
id: 'user-list',
title: '用户列表',
path: '/pages/mall/admin/user-management' // 默认Tab
},
{
id: 'user-add',
title: '添加用户',
path: '/pages/mall/admin/user-management?action=add' // 指定Tab
}
]
},
{
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'
}
]
}
])
```
#### 2. 菜单点击处理
```javascript
const handleMenuClick = (menu: any) => {
activeMenu.value = menu.id
// 跳转到默认Tab
uni.navigateTo({ url: menu.path })
}
const handleSubMenuClick = (subMenu: any) => {
activeSubMenu.value = subMenu.id
// 跳转到指定Tab带参数
uni.navigateTo({ url: subMenu.path })
}
```
#### 3. 页面参数处理
```javascript
// 页面Tab配置
const tabs = ref([
{ key: 'user-list', title: '用户列表', icon: 'icon-list' },
{ key: 'user-add', title: '添加用户', icon: 'icon-add' },
{ key: 'category', title: '商品分类', icon: 'icon-category' }
])
const activeTab = ref('user-list')
// 页面加载时处理参数
onLoad((options: any) => {
if (options && options.action) {
if (options.action === 'add') {
activeTab.value = 'user-add'
showAddModal.value = true
}
} else if (options && options.tab) {
if (options.tab === 'category') {
activeTab.value = 'category'
}
}
})
```
#### 4. Tab内容切换
```vue
<!-- Tab栏 -->
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.key"
class="tab-item"
:class="{ 'active': activeTab === tab.key }"
@click="switchTab(tab.key)"
>
<text class="tab-title">{{ tab.title }}</text>
</view>
</view>
<!-- Tab内容 -->
<view v-if="activeTab === 'user-list'">
<!-- 用户列表内容 -->
</view>
<view v-if="activeTab === 'user-add'">
<!-- 添加用户表单 -->
</view>
<view v-if="activeTab === 'category'">
<!-- 商品分类管理 -->
</view>
```
### 功能示例
#### 用户管理页面
- **用户列表Tab**: 显示用户表格、搜索、筛选、分页
- **添加用户Tab**: 显示新增用户表单
#### 商品管理页面
- **商品列表Tab**: 商品表格管理
- **添加商品Tab**: 商品信息表单
- **商品分类Tab**: 分类树形管理
#### 订单管理页面
- **订单列表Tab**: 订单表格展示
- **订单详情Tab**: 订单详细信息
### URL参数映射
| 页面 | 默认Tab | 参数Tab | 功能 |
|------|---------|---------|------|
| 用户管理 | `user-list` | `?action=add``user-add` | 添加用户 |
| 商品管理 | `product-list` | `?action=add``product-add`<br>`?tab=category``category` | 添加商品/分类管理 |
| 订单管理 | `order-list` | `?action=detail``order-detail` | 订单详情 |
| 财务管理 | `finance-overview` | `?tab=withdrawals``withdrawals` | 提现管理 |
| 系统设置 | `basic` | `?tab=security``security`<br>`?tab=email``email` | 安全设置/邮件设置 |
### 样式实现
#### Tab栏样式
```scss
.tab-bar {
display: flex;
background: #ffffff;
border-radius: 8rpx;
padding: 8rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.tab-item {
flex: 1;
padding: 16rpx 24rpx;
border-radius: 6rpx;
cursor: pointer;
transition: all 0.2s;
background: #f5f5f5;
color: #666666;
&.active {
background: #1890ff;
color: #ffffff;
}
}
```
---
*技术栈uni-app-x + Vue 3 + TypeScript + SCSS + ECharts*
*设计风格CRMEB标准版后台*
*开发时间:完全自主开发* 🎊

View File

@@ -11,7 +11,7 @@
window.process = { env: { NODE_ENV: 'development' } };
</script>
<!-- 入口脚本:通过 main.js 间接引入 main.uts确保 MIME 为 JS -->
<script type="module" src="/main.js"></script>
<script type="module" src="/main"></script>
</head>
<body>

217
layouts/admin/README.md Normal file
View 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
View 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>

View 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>

View 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
View 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
View 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>

View 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
View 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>

View 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
View 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
}

View 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
}

View File

@@ -1,13 +1,13 @@
// 简化的main.uts移除i18n依赖
import { createSSRApp } from 'vue'
import App from './App.uvue'
import i18n from '@/uni_modules/i18n/index.uts'
export function createApp() {
const app = createSSRApp(App)
// 注册 i18n 全局属性,使组件可以使用 $t 方法
app.config.globalProperties.$t = (key: string, values?: any, locale?: string): string => {
return i18n.global.t(key, values, locale)
// 简化的$t方法
app.config.globalProperties.$t = (key: string): string => {
return key // 直接返回key不进行翻译
}
return { app }

10
pages-simple.json Normal file
View File

@@ -0,0 +1,10 @@
{
"pages": [
{
"path": "pages/minimal",
"style": {
"navigationBarTitleText": "最小测试"
}
}
]
}

View File

@@ -6,298 +6,55 @@
"navigationBarTitleText": "管理后台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/consumer/index",
"style": {
"navigationBarTitleText": "商城首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/boot",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/user/register",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/user/forgot-password",
"style": {
"navigationBarTitleText": "忘记密码"
}
},
{
"path": "pages/user/center",
"style": {
"navigationBarTitleText": "用户中心"
}
},
{
"path": "pages/user/profile",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/user/terms",
"style": {
"navigationBarTitleText": "用户协议与隐私政策"
}
},
{
"path": "pages/mall/merchant/index",
"style": {
"navigationBarTitleText": "商家中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/delivery/index",
"style": {
"navigationBarTitleText": "配送中心",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/service/index",
"style": {
"navigationBarTitleText": "客服工作台",
"navigationStyle": "custom"
}
},
{
"path": "pages/mall/analytics/index",
"style": {
"navigationBarTitleText": "数据分析",
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "pages/mall",
"pages": [
{
"path": "consumer/product-detail",
"style": {
"navigationBarTitleText": "商品详情"
}
},
{
"path": "consumer/order-detail",
"style": {
"navigationBarTitleText": "订单详情"
}
},
{
"path": "consumer/profile",
"style": {
"navigationBarTitleText": "个人中心"
}
},
{
"path": "consumer/subscription/plan-list",
"style": {
"navigationBarTitleText": "软件订阅"
}
},
{
"path": "consumer/subscription/plan-detail",
"style": {
"navigationBarTitleText": "订阅详情"
}
},
{
"path": "consumer/subscription/subscribe-checkout",
"style": {
"navigationBarTitleText": "确认订阅"
}
},
{
"path": "consumer/subscription/my-subscriptions",
"style": {
"navigationBarTitleText": "我的订阅"
}
},
{
"path": "admin/subscription/plan-management",
"style": {
"navigationBarTitleText": "订阅方案管理"
}
},
{
"path": "admin/subscription/user-subscriptions",
"style": {
"navigationBarTitleText": "用户订阅管理"
}
},
{
"path": "admin/user-management",
"style": {
"navigationBarTitleText": "用户管理"
}
},
{
"path": "admin/merchant-management",
"style": {
"navigationBarTitleText": "商家管理"
"navigationBarTitleText": "用户管理",
"navigationStyle": "custom"
}
},
{
"path": "admin/product-management",
"style": {
"navigationBarTitleText": "商品管理"
"navigationBarTitleText": "商品管理",
"navigationStyle": "custom"
}
},
{
"path": "admin/order-management",
"style": {
"navigationBarTitleText": "订单管理"
}
},
{
"path": "admin/coupon-management",
"style": {
"navigationBarTitleText": "优惠券管理"
}
},
{
"path": "admin/delivery-management",
"style": {
"navigationBarTitleText": "配送管理"
"navigationBarTitleText": "订单管理",
"navigationStyle": "custom"
}
},
{
"path": "admin/finance-management",
"style": {
"navigationBarTitleText": "财务管理"
"navigationBarTitleText": "财务管理",
"navigationStyle": "custom"
}
},
{
"path": "admin/marketing-management",
"path": "admin/user-statistics",
"style": {
"navigationBarTitleText": "营销管理"
"navigationBarTitleText": "用户统计",
"navigationStyle": "custom"
}
},
{
"path": "admin/system-settings",
"style": {
"navigationBarTitleText": "系统设置"
}
},
{
"path": "admin/notifications",
"style": {
"navigationBarTitleText": "通知中心"
}
},
{
"path": "admin/merchant-review",
"style": {
"navigationBarTitleText": "商家审核"
}
},
{
"path": "admin/product-review",
"style": {
"navigationBarTitleText": "商品审核"
}
},
{
"path": "admin/refund-review",
"style": {
"navigationBarTitleText": "退款处理"
}
},
{
"path": "admin/complaints",
"style": {
"navigationBarTitleText": "投诉处理"
}
},
{
"path": "admin/activity-log",
"style": {
"navigationBarTitleText": "活动日志"
}
},
{
"path": "nfc/security/index",
"style": {
"navigationBarTitleText": "安保工作台",
"enablePullDownRefresh": true,
"backgroundColor": "#f8f9fa"
"navigationBarTitleText": "系统设置",
"navigationStyle": "custom"
}
}
]
}
],
"tabBar": {
"custom": true,
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/mall/consumer/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
},
{
"pagePath": "pages/mall/consumer/category",
"iconPath": "static/tab-category.png",
"selectedIconPath": "static/tab-category-current.png",
"text": "分类"
},
{
"pagePath": "pages/mall/consumer/cart",
"iconPath": "static/tab-cart.png",
"selectedIconPath": "static/tab-cart-current.png",
"text": "购物车"
},
{
"pagePath": "pages/mall/consumer/profile",
"iconPath": "static/tab-profile.png",
"selectedIconPath": "static/tab-profile-current.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "mall",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
},
"condition": {
"current": 0,
"list": [
{
"name": "管理端首页",
"path": "pages/mall/admin/index"
},
{
"name": "启动页(登录态判断)",
"path": "pages/user/boot"
},
{
"name": "登录页",
"path": "pages/user/login"
}
]
}
}

View File

@@ -1,4 +1,3 @@
<!-- 活动日志页面 -->
<template>
<view class="activity-log">
<view class="page-header">

View File

@@ -1,4 +1,3 @@
<!-- 投诉处理页面 -->
<template>
<view class="complaints">
<view class="page-header">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
<!-- 营销管理页面 -->
<template>
<view class="marketing-management">
<!-- 页面标题 -->

View File

@@ -1,4 +1,3 @@
<!-- 商家审核页面 -->
<template>
<view class="merchant-review">
<view class="page-header">

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
<!-- 商品审核页面 -->
<template>
<view class="product-review">
<view class="page-header">

View File

@@ -1,4 +1,3 @@
<!-- 管理端 - 个人中心 -->
<template>
<view class="admin-profile">
<!-- 管理员信息头部 -->

View File

@@ -1,4 +1,3 @@
<!-- 退款审核页面 -->
<template>
<view class="refund-review">
<view class="page-header">

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
<!-- 管理端 - 用户详情页 -->
<template>
<view class="user-detail-page">
<!-- 用户基本信息 -->

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,764 @@
<template>
<AdminLayout current-page="statistics">
<view class="user-statistics-page">
<!-- 筛选条件栏 -->
<view class="filter-section">
<view class="filter-row">
<view class="filter-left">
<view class="filter-item">
<text class="filter-label">用户渠道:</text>
<picker
mode="selector"
:range="channelOptions"
:value="selectedChannel"
@change="handleChannelChange"
>
<view class="filter-select">
<text>{{ channelOptions[selectedChannel] }}</text>
<text class="iconfont icon-down"></text>
</view>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">日期范围:</text>
<view class="date-range">
<picker
mode="date"
:value="startDate"
:start="minDate"
:end="maxDate"
@change="handleStartDateChange"
>
<view class="date-input">
<text>{{ startDate || '开始日期' }}</text>
</view>
</picker>
<text class="date-separator">-</text>
<picker
mode="date"
:value="endDate"
:start="minDate"
:end="maxDate"
@change="handleEndDateChange"
>
<view class="date-input">
<text>{{ endDate || '结束日期' }}</text>
</view>
</picker>
</view>
</view>
</view>
<view class="filter-right">
<button class="btn-secondary" @click="handleSearch">
<text class="iconfont icon-search"></text>
查询
</button>
<button class="btn-primary" @click="handleExport">
<text class="iconfont icon-export"></text>
导出
</button>
</view>
</view>
</view>
<!-- 指标概览 -->
<view class="metrics-section">
<view class="metrics-row">
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-users"></text>
</view>
<view class="metric-content">
<text class="metric-title">累计用户</text>
<text class="metric-value">{{ formatNumber(totalUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ userGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-eye"></text>
</view>
<view class="metric-content">
<text class="metric-title">访客数</text>
<text class="metric-value">{{ formatNumber(totalVisitors) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ visitorGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-view"></text>
</view>
<view class="metric-content">
<text class="metric-title">浏览量</text>
<text class="metric-value">{{ formatNumber(totalPageViews) }}</text>
<view class="metric-change down">
<text class="iconfont icon-down"></text>
<text class="change-text">{{ pageViewDecline }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-user-add"></text>
</view>
<view class="metric-content">
<text class="metric-title">新增用户</text>
<text class="metric-value">{{ formatNumber(newUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ newUserGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-shopping"></text>
</view>
<view class="metric-content">
<text class="metric-title">成交用户</text>
<text class="metric-value">{{ formatNumber(convertedUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ conversionGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
<view class="metric-card">
<view class="metric-icon">
<text class="iconfont icon-vip"></text>
</view>
<view class="metric-content">
<text class="metric-title">付费会员</text>
<text class="metric-value">{{ formatNumber(vipUsers) }}</text>
<view class="metric-change up">
<text class="iconfont icon-up"></text>
<text class="change-text">{{ vipGrowth }}%</text>
<text class="change-desc">较上月</text>
</view>
</view>
</view>
</view>
</view>
<!-- 用户趋势图表 -->
<view class="chart-section">
<view class="admin-card">
<view class="admin-card-header">
<text class="admin-card-title">用户数据趋势分析</text>
</view>
<view class="admin-card-body">
<!-- 图表图例 -->
<view class="chart-legend">
<view class="legend-item" v-for="item in trendLegend" :key="item.key">
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
<text class="legend-text">{{ item.name }}</text>
</view>
</view>
<!-- 多折线图表容器 -->
<view class="multi-line-chart">
<!-- 图表区域 -->
<view class="chart-area">
<!-- 模拟多折线图 -->
<view class="line-container" v-for="(line, index) in trendLines" :key="line.key">
<view class="line-points">
<view
v-for="(point, pIndex) in line.data"
:key="pIndex"
class="line-point"
:style="{
left: (pIndex * 100 / (line.data.length - 1)) + '%',
bottom: point.height + '%',
backgroundColor: line.color
}"
></view>
</view>
</view>
<!-- X轴标签 -->
<view class="x-axis-labels">
<text class="axis-label" v-for="date in chartDates" :key="date">{{ date }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</AdminLayout>
</template>
<script setup lang="uts">
import { ref } from 'vue'
import AdminLayout from '@/layouts/admin/index.uvue'
// 筛选条件
const selectedChannel = ref(0)
const channelOptions = ['全部渠道', '自然流量', '搜索引擎', '社交媒体', '广告投放', '其他']
const startDate = ref('')
const endDate = ref('')
const minDate = '2020-01-01'
const maxDate = new Date().toISOString().split('T')[0]
// 指标数据
const totalUsers = ref(32456)
const userGrowth = ref(12.5)
const totalVisitors = ref(156789)
const visitorGrowth = ref(8.3)
const totalPageViews = ref(456123)
const pageViewDecline = ref(3.2)
const newUsers = ref(1234)
const newUserGrowth = ref(15.7)
const convertedUsers = ref(5678)
const conversionGrowth = ref(9.4)
const vipUsers = ref(1234)
const vipGrowth = ref(22.1)
// 图表数据
const chartDates = ['01-01', '01-08', '01-15', '01-22', '01-29', '02-05', '02-12']
const trendLegend = [
{ key: 'newUsers', name: '新增用户', color: '#1890ff' },
{ key: 'visitors', name: '访客数', color: '#52c41a' },
{ key: 'pageViews', name: '浏览量', color: '#faad14' },
{ key: 'conversions', name: '成交用户', color: '#f5222d' },
{ key: 'vipUsers', name: '付费会员', color: '#722ed1' }
]
// 趋势线数据
const trendLines = ref([
{
key: 'newUsers',
color: '#1890ff',
data: [
{ value: 120, height: 12 },
{ value: 180, height: 18 },
{ value: 250, height: 25 },
{ value: 320, height: 32 },
{ value: 280, height: 28 },
{ value: 350, height: 35 },
{ value: 420, height: 42 }
]
},
{
key: 'visitors',
color: '#52c41a',
data: [
{ value: 450, height: 45 },
{ value: 520, height: 52 },
{ value: 580, height: 58 },
{ value: 620, height: 62 },
{ value: 550, height: 55 },
{ value: 680, height: 68 },
{ value: 750, height: 75 }
]
},
{
key: 'pageViews',
color: '#faad14',
data: [
{ value: 680, height: 68 },
{ value: 720, height: 72 },
{ value: 850, height: 85 },
{ value: 920, height: 92 },
{ value: 780, height: 78 },
{ value: 950, height: 95 },
{ value: 1000, height: 100 }
]
},
{
key: 'conversions',
color: '#f5222d',
data: [
{ value: 45, height: 4.5 },
{ value: 52, height: 5.2 },
{ value: 68, height: 6.8 },
{ value: 75, height: 7.5 },
{ value: 62, height: 6.2 },
{ value: 85, height: 8.5 },
{ value: 95, height: 9.5 }
]
},
{
key: 'vipUsers',
color: '#722ed1',
data: [
{ value: 12, height: 1.2 },
{ value: 15, height: 1.5 },
{ value: 22, height: 2.2 },
{ value: 28, height: 2.8 },
{ value: 25, height: 2.5 },
{ value: 35, height: 3.5 },
{ value: 42, height: 4.2 }
]
}
])
// 方法
const handleChannelChange = (e: any) => {
selectedChannel.value = e.detail.value
}
const handleStartDateChange = (e: any) => {
startDate.value = e.detail.value
}
const handleEndDateChange = (e: any) => {
endDate.value = e.detail.value
}
const handleSearch = () => {
uni.showToast({
title: '数据已更新',
icon: 'success'
})
}
const handleExport = () => {
uni.showToast({
title: '导出功能开发中',
icon: 'none'
})
}
const formatNumber = (num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k'
}
return num.toString()
}
</script>
<style>
/* ===== 用户统计页面样式 ===== */
.user-statistics-page {
width: 100%;
}
/* ===== 筛选条件栏 ===== */
.filter-section {
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.filter-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.filter-left {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
.filter-right {
display: flex;
gap: 16px;
}
.filter-item {
display: flex;
align-items: center;
gap: 12px;
}
.filter-label {
font-size: 14px;
color: #666666;
white-space: nowrap;
}
.filter-select {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #ffffff;
cursor: pointer;
min-width: 120px;
font-size: 14px;
}
.date-range {
display: flex;
align-items: center;
gap: 8px;
}
.date-input {
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #ffffff;
text-align: center;
cursor: pointer;
font-size: 14px;
min-width: 120px;
}
.date-separator {
color: #666666;
font-size: 14px;
}
.btn-primary {
background-color: #1890ff;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.btn-secondary {
background-color: #ffffff;
color: #666666;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
/* ===== 指标概览 ===== */
.metrics-section {
margin-bottom: 24px;
}
.metrics-row {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.metric-card {
flex: 1;
min-width: 280px;
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.metric-icon {
width: 56px;
height: 56px;
background: linear-gradient(135deg, #1890ff 0%, #36cfc9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 24px;
flex-shrink: 0;
}
.metric-content {
flex: 1;
}
.metric-title {
display: block;
font-size: 14px;
color: #666666;
margin-bottom: 8px;
}
.metric-value {
display: block;
font-size: 24px;
font-weight: 600;
color: #262626;
margin-bottom: 8px;
}
.metric-change {
display: flex;
align-items: center;
font-size: 12px;
border-radius: 12px;
padding: 4px 8px;
}
.metric-change.up {
background-color: #f6ffed;
color: #52c41a;
}
.metric-change.down {
background-color: #fff2f0;
color: #ff4d4f;
}
.change-text {
margin: 0 4px;
font-weight: 500;
}
.change-desc {
color: #999999;
}
/* ===== 图表区域 ===== */
.chart-section {
margin-bottom: 24px;
}
.admin-card {
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.admin-card-header {
padding: 24px 24px 0 24px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.admin-card-title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
.admin-card-body {
padding: 0 24px 24px 24px;
}
/* ===== 图表图例 ===== */
.chart-legend {
display: flex;
justify-content: center;
gap: 32px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
}
.legend-text {
font-size: 14px;
color: #666666;
}
/* ===== 多折线图表 ===== */
.multi-line-chart {
height: 400px;
position: relative;
background-color: #ffffff;
border: 1px solid #e8e8e8;
border-radius: 6px;
}
.chart-area {
position: absolute;
top: 40px;
left: 60px;
right: 40px;
bottom: 60px;
}
.line-container {
position: absolute;
width: 100%;
height: 100%;
}
.line-points {
position: relative;
width: 100%;
height: 100%;
}
.line-point {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid #ffffff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
transform: translate(-50%, -50%);
}
.x-axis-labels {
position: absolute;
bottom: -40px;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 0 30px;
}
.axis-label {
font-size: 12px;
color: #999999;
text-align: center;
}
/* ===== 响应式设计 ===== */
@media (max-width: 1200px) {
.metrics-row {
flex-wrap: wrap;
}
.metric-card {
min-width: 45%;
flex: 0 0 auto;
}
}
@media (max-width: 768px) {
.filter-row {
flex-direction: column;
align-items: stretch;
}
.filter-left {
flex-direction: column;
gap: 16px;
}
.filter-right {
justify-content: center;
}
.metrics-row {
flex-direction: column;
}
.metric-card {
min-width: auto;
width: 100%;
}
.user-statistics-page {
padding: 16px;
}
.filter-section,
.chart-section {
margin-bottom: 16px;
}
.admin-card-header,
.admin-card-body {
padding-left: 16px;
padding-right: 16px;
}
.chart-legend {
gap: 16px;
}
}
/* ===== 图标字体 ===== */
.iconfont {
font-family: 'iconfont';
font-size: 14px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-up:before {
content: '↑';
}
.icon-down:before {
content: '↓';
}
.icon-users:before {
content: '👥';
}
.icon-eye:before {
content: '👁️';
}
.icon-view:before {
content: '📊';
}
.icon-user-add:before {
content: '👤';
}
.icon-shopping:before {
content: '🛒';
}
.icon-vip:before {
content: '👑';
}
.icon-search:before {
content: '🔍';
}
.icon-export:before {
content: '📤';
}
.icon-down:before {
content: '▼';
}
</style>

View File

@@ -1,4 +1,3 @@
<!-- 消费者端首页 - 严格UTS Android规范 -->
<template>
<view class="consumer-home">
<!-- 顶部搜索栏 -->

29
pages/minimal.uvue Normal file
View File

@@ -0,0 +1,29 @@
<template>
<view class="container">
<text class="title">Minimal Test Page</text>
<text class="content">This is a minimal test page to check if uni-app-x compilation works.</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>

16
pages/test-minimal.uvue Normal file
View File

@@ -0,0 +1,16 @@
<template>
<view class="test">
<text>Hello World</text>
</view>
</template>
<script setup lang="uts">
// Minimal test
</script>
<style>
.test {
padding: 20px;
text-align: center;
}
</style>

38
tsconfig.json Normal file
View File

@@ -0,0 +1,38 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
},
"types": ["@dcloudio/types"]
},
"include": [
"src/**/*",
"pages/**/*",
"components/**/*",
"layouts/**/*",
"utils/**/*",
"types/**/*",
"*.uts",
"*.uvue"
],
"exclude": [
"node_modules",
"dist",
"unpackage"
]
}