保留页面布局

This commit is contained in:
2026-02-05 11:36:55 +08:00
parent d51e6a8f72
commit 821205b18a
15 changed files with 476 additions and 247 deletions

View File

@@ -0,0 +1,85 @@
# 管理后台响应式布局实现指南
本文档总结了商城管理后台从固定布局到响应式布局的改造过程及核心技术点。
## 1. 核心目标
- **多终端适配**:确保后台在桌面端(宽屏)、平板端(中等屏幕)和移动端(窄屏)均能良好展示。
- **自动适配状态管理**:系统能自动感知屏幕宽度并调整侧边栏显隐逻辑。
- **布局平滑过渡**:侧边栏的收起、拉出及内容区的重排应具有良好的动画效果。
## 2. 状态管理 (Store) 扩展
在 [adminNavStore.uts](layouts/admin/store/adminNavStore.uts) 中引入了响应式状态:
- `windowWidth`: 实时存储当前窗口宽度。
- `isMobile`: 计算属性,当宽度小于 768px 时判定为移动端。
- `isMobileMenuOpen`: 控制移动端模式下侧边栏的展开状态。
```typescript
export const windowWidth = ref<number>(1024);
export const isMobile = computed<boolean>(() => windowWidth.value < 768);
export const isMobileMenuOpen = ref<boolean>(false);
```
## 3. 布局架构调整 (AdminLayout)
[AdminLayout.uvue](layouts/admin/AdminLayout.uvue) 负责整体结构的响应式策略:
### 3.1 监听并初始化
`onMounted` 中初始化宽度并监听 `uni.onWindowResize`
```typescript
onMounted(() => {
windowWidth.value = uni.getWindowInfo().windowWidth;
uni.onWindowResize((res) => {
windowWidth.value = res.size.windowWidth;
});
});
```
### 3.2 侧边栏处理
- **桌面端**:侧边栏常规展示,通过 `marginLeft` 为内容区腾出空间。
- **移动端**:侧边栏通过 `position: absolute` 隐藏在屏幕外,通过 `translateX` 动画滑入。同时禁用二级侧边栏的常驻显示。
### 3.3 移动端遮罩与切换
- 引入 `mobile-mask` 遮罩层,点击遮罩自动关闭菜单。
- [AdminHeader](layouts/admin/components/AdminHeader.uvue) 在移动端会显示 “☰” 切换按钮。
## 4. 页面内容重排 (HomeIndex)
[HomeIndex.uvue](layouts/admin/pages/HomeIndex.uvue) 利用 CSS 媒体查询实现内容区域的自适应:
### 4.1 KPI 卡片流式布局
使用 `flex-wrap: wrap` 配合 `min-width`
- **布局策略**:设置 `min-width: 250px` 作为安全边界,确保在 1200px 分辨率下依然能并排展示 4 个卡片。
- **动态列数**
- **宽屏 (>1200px)**: 4个卡片并排。
- **中屏 (768px - 1200px)**: 强制 2 个并排。
- **窄屏 (<768px)**: 1个卡片占满整行。
- **高度控制**:统一固定为 `200px`
### 4.2 图表响应式
- **垂直排列**:底部两个并排的图表在小屏下自动切换为垂直排列(`flex-direction: column`)。
- **组件自适应**:图表组件内部利用父容器宽度自动伸缩。
- **头部压缩**:移动端下,图表的配置项(如日期切换标签)由横向改为纵向,避免溢出。
## 5. 组件化实践 (KpiMiniCard)
统一使用了 [KpiMiniCard](pages/mall/admin/homePage/components/KpiMiniCard.uvue) 组件,确保:
- 样式一致性。
- 代码复用性。
- 内部样式的可维护性。
## 6. 使用建议
- 后续开发新页面时,请优先使用 `stats-row``stat-card` 进行布局。
- 对于复杂的表格页面,建议在移动端隐藏非核心列,或使用横向滚动条展示。
- 所有的宽度判定建议遵循 768px 这一标准断点。

View File

@@ -11,6 +11,20 @@
- **解决方案**: 确保替换操作覆盖文件的完整生命周期,或者在发现 500 错误时检查文件末尾是否有残留的旧标签。
- **预防**: 优先使用 `create_file` 或子代理重写整个文件,而非局部替换复杂的 SFC 结构。
#### **原因十二KPI 统计网格响应式不一致 (用户体验红线)**
- **现象**: 某些宽度下出现一行 3 个卡片,导致视觉不平衡或数据展示拥挤。
- **原因**: 使用了 `repeat(auto-fit/auto-fill, ...)` 或基于 `min-width` 的 flex 自动布局。
- **解决方案**:
1. 使用全局统一类 `.kpi-grid`
2. 严禁使用 `auto-fit/auto-fill`
3. 必须显式使用视图断点拦截:
- `>= 1200px`: 固定 4 列 (`grid-template-columns: repeat(4, minmax(0, 1fr))`)。
- `768px - 1199px`: 固定 2 列 (`grid-template-columns: repeat(2, minmax(0, 1fr))`)。
- `< 768px`: 固定 1 列 (`grid-template-columns: repeat(1, minmax(0, 1fr))`)。
4. 使用 `minmax(0, 1fr)` 配分子项 `min-width: 0` 确保在任何容器宽度下网格不被撑爆。
- **强制规则**: 任何页面都不允许出现一行 3 个卡片的情况。
## 🛠️ 完整修复流程
```

View File

@@ -81,7 +81,7 @@ const props = withDefaults(defineProps<{
const trendArrow = computed((): string => {
if (props.trend === 'up') return '▲'
if (props.trend === 'down') return '▼'
return ''
return ''
})
const trendClass = computed((): string => {
@@ -94,33 +94,43 @@ const trendClass = computed((): string => {
<style>
.kpi-card{
background-color:#ffffff;
border:1px solid #ebeef5;
border-radius:6px;
border-radius:4px;
padding:16px;
box-shadow:0 2px 12px rgba(0,0,0,0.04);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
height: 200px; /* 固定高度 */
min-width: 0; /* 允许由父级 grid 容器决定宽度,防止在 4 列布局时撑爆容器 */
display: flex;
flex-direction: column;
overflow: hidden;
}
.kpi-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* Header */
.kpi-header{
display:flex;
flex-direction: row;
align-items:center;
justify-content:space-between;
gap:12px;
margin-bottom: 8px;
flex-shrink: 0;
.kpi-title{
font-size:14px;
color:#303133;
font-weight:600;
color:#666666;
font-weight:400;
}
.kpi-tag{
padding:2px 8px;
border-radius:4px;
border:1px solid #e1f3d8;
background:#f0f9eb;
padding:1px 6px;
border-radius:2px;
background-color: #e8f4ff;
}
.kpi-tag-text{
font-size:12px;
color:#67c23a;
color:#1890ff;
}
}
@@ -128,60 +138,82 @@ const trendClass = computed((): string => {
/* Body */
.kpi-body{
margin-top:10px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.kpi-main-value{
font-size:32px;
font-weight:600;
color:#303133;
line-height:40px;
font-size:30px;
font-weight:500;
color:#262626;
line-height:1.2;
margin-bottom: 4px;
}
/* “昨日 / 日环比” */
.kpi-meta{
margin-top:8px;
display:flex;
flex-direction: row;
align-items:center;
justify-content:flex-start;
gap:12px;
flex-wrap:wrap;
gap:8px;
padding-bottom:12px;
border-bottom:1px solid #f0f0f0;
margin-bottom: auto; /* 将 footer 顶到底部 */
flex-wrap: nowrap; /* 不允许换行,依靠父容器 min-width 保证空间 */
}
.kpi-meta-text{
font-size:12px;
color:#909399;
color:#8c8c8c;
flex-shrink: 0;
}
.kpi-meta-right{
display:flex;
flex-direction: row;
align-items:center;
gap:6px;
gap:4px;
flex-shrink: 0;
}
.kpi-trend-arrow{
font-size:12px;
font-weight: 500;
}
.kpi-trend-arrow.is-up{ color:#f56c6c; }
.kpi-trend-arrow.is-down{ color:#67c23a; }
.kpi-trend-arrow.is-flat{ color:#909399; }
.kpi-trend-arrow.is-up{ color:#ff4d4f; }
.kpi-trend-arrow.is-down{ color:#52c41a; }
.kpi-trend-arrow.is-flat{ color:#8c8c8c; }
.kpi-divider{
height:1px;
background:#ebeef5;
margin:12px 0;
display: none; /* 已整合到 meta 的 border-bottom */
}
/* Footer */
.kpi-footer{
display:flex;
flex-direction: row;
align-items:center;
justify-content:space-between;
gap:12px;
flex-shrink: 0;
}
.kpi-footer-left{
font-size:12px;
color:#909399;
color:#8c8c8c;
white-space: nowrap;
}
.kpi-footer-right{
font-size:12px;
color:#909399;
color:#262626;
font-weight:500;
white-space: nowrap;
}
}
/* 响应式微调 */
@media screen and (max-width: 480px) {
.kpi-main-value {
font-size: 26px !important;
}
.kpi-card {
padding: 12px !important;
}
}
</style>

View File

@@ -160,7 +160,7 @@ const orderData = ref([
typeColor: 'blue',
cancelStatus: '用户已取消',
product: {
img: 'https://img.crmeb.com/crmeb_demo/75211.png',
img: '/static/logo.png',
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室 超高清手机便携投影机 (4K超清 支持...'
},
user: { phone: '188****4074', id: '82694' },
@@ -176,7 +176,7 @@ const orderData = ref([
typeColor: 'purple',
cancelStatus: '',
product: {
img: 'https://img.crmeb.com/crmeb_demo/75211.png',
img: '/static/logo.png',
name: '阿迪达斯官网 adidas BBALL CAP COT 男女训练运动帽子FQ5270 传奇墨水...'
},
user: { phone: '你就给', id: '82703' },
@@ -192,7 +192,7 @@ const orderData = ref([
typeColor: 'green',
cancelStatus: '',
product: {
img: 'https://img.crmeb.com/crmeb_demo/75211.png',
img: '/static/logo.png',
name: 'UR2024夏季新款女装复古纯欲氛围感一字肩短款T恤衫UWG440060'
},
user: { phone: '王毅不睡了', id: '82689' },
@@ -208,7 +208,7 @@ const orderData = ref([
typeColor: 'blue',
cancelStatus: '',
product: {
img: 'https://img.crmeb.com/crmeb_demo/75211.png',
img: '/static/logo.png',
name: '爱奇艺智能 奇遇LT01 投影仪 家用卧室 超高清手机便携投影机 (4K超清 支持...'
},
user: { phone: '177****8361', id: '82697' },

View File

@@ -16,8 +16,8 @@
</view>
</view>
<!-- 统计指标网格 -->
<view class="stat-grid">
<!-- 统计指标网格 (使用统一响应式网格) -->
<view class="kpi-grid">
<view v-for="(item, index) in statItems" :key="index" class="stat-card">
<view class="stat-main">
<view class="icon-box" :style="{ backgroundColor: item.bgColor }">
@@ -367,19 +367,12 @@ function initChart() {
.btn-query { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
.btn-export { background: #1890ff; color: #fff; font-size: 14px; height: 32px; padding: 0 15px; border-radius: 4px; border: none; }
.stat-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
/* stat-grid 已废弃,由全局 kpi-grid 接管 */
.stat-card {
width: calc(33.33% - 11px);
background: #fff;
border-radius: 8px;
padding: 20px;
min-width: 0;
}
.stat-main {

View File

@@ -74,7 +74,7 @@
</view>
<view class="item-content">
<view class="avatar-upload">
<image class="avatar-preview" src="https://img.crmeb.com/crmeb_demo/75211.png" mode="aspectFill" />
<image class="avatar-preview" src="/static/logo.png" mode="aspectFill" />
<view class="upload-mask">
<text class="upload-icon">+</text>
</view>

View File

@@ -24,14 +24,14 @@
</view>
</view>
<!-- 用户概况卡片区 -->
<!-- 用户概况卡片区 (使用统一响应式网格) -->
<view class="section-card">
<view class="section-header">
<text class="section-title">用户概况</text>
<text class="info-icon">ⓘ</text>
</view>
<view class="kpi-row">
<view class="kpi-grid">
<view class="kpi-card" v-for="item in kpiData" :key="item.title">
<view class="kpi-icon-box" :style="{ backgroundColor: item.bg }">
<text class="kpi-icon">{{ item.icon }}</text>
@@ -221,22 +221,14 @@ function onExport() {
color: #bfbfbf;
}
.kpi-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 40px;
}
/* kpi-row 已废弃,采用全局 kpi-grid */
.kpi-card {
flex: 1;
min-width: 200px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding: 8px;
min-width: 0; /* 允许收缩 */
}
.kpi-icon-box {
@@ -296,6 +288,31 @@ function onExport() {
width: 100%;
}
@media (max-width: 1200px) {
.filter-card {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.filter-item {
width: 100%;
}
.select-box, .date-picker-box {
flex: 1;
}
.analysis-row {
flex-direction: column;
}
.map-col, .gender-col {
width: 100%;
flex: none;
}
}
.map-col {
flex: 7;
}

View File

@@ -154,15 +154,15 @@ const isAllChecked = ref(false)
const activeDropdownId = ref<string | null>(null)
const userList = ref([
{ id: '77414', avatar: 'https://img.crmeb.com/crmeb_demo/77414.png', nickname: '199****0268', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '199****0268', userType: '公众号', balance: '88888.00', checked: false },
{ id: '75311', avatar: 'https://img.crmeb.com/crmeb_demo/75311.png', nickname: 'wljbhg', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100002.00', checked: false },
{ id: '75305', avatar: 'https://img.crmeb.com/crmeb_demo/75305.png', nickname: '相见欢', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75296', avatar: 'https://img.crmeb.com/crmeb_demo/75296.png', nickname: '..', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75293', avatar: 'https://img.crmeb.com/crmeb_demo/75293.png', nickname: '钟(钏)华', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75289', avatar: 'https://img.crmeb.com/crmeb_demo/75289.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75257', avatar: 'https://img.crmeb.com/crmeb_demo/75257.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75226', avatar: 'https://img.crmeb.com/crmeb_demo/75226.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75211', avatar: 'https://img.crmeb.com/crmeb_demo/75211.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }
{ id: '77414', avatar: '/static/logo.png', nickname: '199****0268', isMember: '否', level: '无', group: '无', spreadLevel: '', phone: '199****0268', userType: '公众号', balance: '88888.00', checked: false },
{ id: '75311', avatar: '/static/logo.png', nickname: 'wljbhg', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100002.00', checked: false },
{ id: '75305', avatar: '/static/logo.png', nickname: '相见欢', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75296', avatar: '/static/logo.png', nickname: '..', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75293', avatar: '/static/logo.png', nickname: '钟(钏)华', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75289', avatar: '/static/logo.png', nickname: '小二上酒', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75257', avatar: '/static/logo.png', nickname: '5+7', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75226', avatar: '/static/logo.png', nickname: '慢步前行', isMember: '是', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false },
{ id: '75211', avatar: '/static/logo.png', nickname: '难得糊涂', isMember: '否', level: '无', group: 'A类客户', spreadLevel: '', phone: '', userType: '公众号', balance: '100000.00', checked: false }
])
function onSearch() {