保留页面布局

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

@@ -9,6 +9,9 @@
</script>
<style>
/* 引入管理后台通用响应式样式 */
@import "@/layouts/admin/styles/admin-responsive.css";
/* ===== 全局重置样式 ===== */
* {
margin: 0;

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@@ -1,7 +1,16 @@
<template>
<view class="layout-root">
<!-- 移动端遮罩层 -->
<view
v-if="isMobile && isMobileMenuOpen"
class="mobile-mask"
@click="isMobileMenuOpen = false"
></view>
<!-- 主侧边栏 (CRMEB风格) -->
<AdminAside
class="admin-sidebar"
:class="{ 'mobile-aside-open': isMobileMenuOpen }"
:collapsed="isMainAsideCollapsed"
:topMenus="topMenus"
:activeTopMenuId="activeTopMenuId"
@@ -12,7 +21,7 @@
<!-- 二级侧边栏 (CRMEB风格 - 内容区左侧) -->
<AdminSubSider
v-if="showSubSider"
v-if="showSubSider && !isMobile"
:topMenuTitle="activeTopMenuTitle"
:groups="activeGroups"
:routes="activeRoutes"
@@ -25,19 +34,22 @@
<!-- 右侧内容区 -->
<view
class="main"
:style="{ marginLeft: mainLeft }"
:style="{ marginLeft: isMobile ? '0' : mainLeft }"
>
<!-- 顶部导航栏 -->
<AdminHeader
:breadcrumb="breadcrumb"
:hasNotification="hasNotification"
:isMobile="isMobile"
@toggle-mobile-menu="isMobileMenuOpen = !isMobileMenuOpen"
@search="onSearch"
@refresh="onRefresh"
@notify="onNotify"
/>
<!-- 标签页 (CRMEB风格) -->
<!-- 标签页 (CRMEB风格) - 移动端可以隐藏或滚动 -->
<AdminTagsView
v-if="!isMobile"
:tabs="tabs"
:activeTabId="activeRouteId"
@tab-click="onTabClick"
@@ -48,7 +60,7 @@
<!-- 内容展示区 (内部路由渲染) -->
<view class="content-scroll">
<view class="content-inner">
<view class="content-inner" :style="{ padding: isMobile ? '12px' : '16px' }">
<component :is="currentComponent" />
</view>
<AdminFooter />
@@ -80,6 +92,9 @@ import {
tabs,
isMainAsideCollapsed,
showSubSider,
windowWidth,
isMobile,
isMobileMenuOpen,
openRoute,
closeTab,
closeOtherTabs,
@@ -194,6 +209,18 @@ function onNotify(): void {
onMounted(() => {
initNavState()
// 初始化窗口宽度
windowWidth.value = uni.getWindowInfo().windowWidth
// 监听窗口变化
uni.onWindowResize((res) => {
windowWidth.value = res.size.windowWidth
// 窗口变大时自动关闭移动端菜单
if (windowWidth.value >= 768) {
isMobileMenuOpen.value = false
}
})
})
</script>
@@ -204,6 +231,32 @@ onMounted(() => {
width: 100%;
min-height: 100vh;
background: #f0f2f5;
position: relative;
}
/* 移动端侧边栏样式 */
.mobile-aside {
position: absolute;
left: -100px; /* 隐藏在左侧 */
top: 0;
bottom: 0;
z-index: 1001;
transition: transform 0.3s ease;
background: #fff;
}
.mobile-aside-open {
transform: translateX(100px); /* 移入视图 */
}
.mobile-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1000;
}
.main {
@@ -213,11 +266,35 @@ onMounted(() => {
min-height: 100vh;
transition: margin-left 0.3s ease;
background: #f0f2f5;
width: 100%;
}
/* 响应式强制覆盖 */
@media screen and (max-width: 768px) {
.main {
margin-left: 0 !important;
}
/* 强行改变侧边栏布局模式 */
.admin-sidebar {
position: absolute !important;
left: -100px !important; /* 隐藏在左侧,假设 ASIDE_W 是 96 */
top: 0;
bottom: 0;
z-index: 1001;
transition: transform 0.3s ease !important;
}
/* 展开时的状态 */
.mobile-aside-open {
transform: translateX(100px) !important;
}
}
.content-scroll {
flex: 1;
overflow-y: scroll;
overflow-x: auto; /* 允许横向滚动,兼容极端窄屏 */
background: #f0f2f5;
}

View File

@@ -1,15 +1,25 @@
<template>
<view class="header">
<view class="header-left">
<!-- 移动端菜单切换按钮 (CSS 控制显隐) -->
<view class="menu-toggle mobile-only" @click="$emit('toggle-mobile-menu')">
<text class="menu-icon">☰</text>
</view>
<view class="breadcrumb-container desktop-only">
<text class="crumb" v-for="(item, index) in breadcrumb" :key="item.id">
{{ item.title }}
<text v-if="index < breadcrumb.length - 1" class="separator"> / </text>
</text>
</view>
<!-- 移动端简单标题 (CSS 控制显隐) -->
<text class="mobile-title mobile-only">{{ currentTitle }}</text>
</view>
<view class="header-right">
<view class="hbtn" @click="$emit('search')"><text>🔍</text></view>
<view class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
<view v-if="!isMobile" class="hbtn" @click="$emit('refresh')"><text>⟳</text></view>
<view class="hbtn" @click="$emit('notify')">
<text>🔔</text>
<view class="dot" v-if="hasNotification"></view>
@@ -19,16 +29,27 @@
</template>
<script setup lang="uts">
defineProps<{
import { computed } from 'vue'
const props = defineProps<{
breadcrumb: Array<{id: string, title: string}>
hasNotification: boolean
isMobile: boolean
}>()
defineEmits<{
(e:'search'): void
(e:'refresh'): void
(e:'notify'): void
(e:'toggle-mobile-menu'): void
}>()
const currentTitle = computed((): string => {
if (props.breadcrumb.length > 0) {
return props.breadcrumb[props.breadcrumb.length - 1].title
}
return '管理后台'
})
</script>
<style>
@@ -49,11 +70,54 @@ defineEmits<{
align-items: center;
}
.menu-toggle {
margin-right: 12px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-icon {
font-size: 20px;
color: #333;
}
.mobile-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.breadcrumb-container {
display: flex;
flex-direction: row;
align-items: center;
}
.crumb {
color: #374151;
font-size: 14px;
}
/* 响应式控制 */
.mobile-only {
display: none;
}
@media screen and (max-width: 768px) {
.desktop-only {
display: none !important;
}
.mobile-only {
display: flex !important;
}
.header-right {
gap: 5px;
}
}
.separator {
color: #d1d5db;
margin: 0 8px;

View File

@@ -1,74 +1,54 @@
<template>
<view class="home-page">
<!-- 数据统计卡片行 -->
<view class="stats-row">
<!-- 销售额卡片 -->
<view class="stat-card">
<view class="card-header">
<text class="card-title">销售额</text>
<view class="tag today">今日</view>
</view>
<view class="card-value">91.1</view>
<view class="card-meta">
<text class="meta-text">昨日 2740</text>
<text class="meta-trend down">日环比 -96.67% ▼</text>
</view>
<view class="card-footer">
<text class="footer-label">本月销售额</text>
<text class="footer-value">2831.1元</text>
</view>
</view>
<!-- 数据统计卡片行 (使用统一响应式网格) -->
<view class="kpi-grid">
<KpiMiniCard
class="stat-card"
title="销售额"
tagText="今日"
:valueText="statsData.sales.today.toString()"
:metaLeft="'昨日 ' + statsData.sales.yesterday"
:metaRight="'日环比 ' + statsData.sales.trend + '%'"
:trend="statsData.sales.trend > 0 ? 'up' : 'down'"
footerLeftText="本月销售额"
:footerRightText="statsData.sales.monthTotal + '元'"
/>
<!-- 用户访问量卡片 -->
<view class="stat-card">
<view class="card-header">
<text class="card-title">用户访问量</text>
<view class="tag today">今日</view>
</view>
<view class="card-value">224</view>
<view class="card-meta">
<text class="meta-text">昨日 136</text>
<text class="meta-trend up">日环比 64.7% ▲</text>
</view>
<view class="card-footer">
<text class="footer-label">本月访问量</text>
<text class="footer-value">360Pv</text>
</view>
</view>
<KpiMiniCard
class="stat-card"
title="用户访问量"
tagText="今日"
:valueText="statsData.visits.today.toString()"
:metaLeft="'昨日 ' + statsData.visits.yesterday"
:metaRight="'日环比 ' + statsData.visits.trend + '%'"
:trend="statsData.visits.trend > 0 ? 'up' : 'down'"
footerLeftText="本月访问量"
:footerRightText="statsData.visits.monthTotal + 'Pv'"
/>
<!-- 订单量卡片 -->
<view class="stat-card">
<view class="card-header">
<text class="card-title">订单量</text>
<view class="tag today">今日</view>
</view>
<view class="card-value">4</view>
<view class="card-meta">
<text class="meta-text">昨日 8</text>
<text class="meta-trend down">日环比 -50% ▼</text>
</view>
<view class="card-footer">
<text class="footer-label">本月订单量</text>
<text class="footer-value">12单</text>
</view>
</view>
<KpiMiniCard
class="stat-card"
title="订单量"
tagText="今日"
:valueText="statsData.orders.today.toString()"
:metaLeft="'昨日 ' + statsData.orders.yesterday"
:metaRight="'日环比 ' + statsData.orders.trend + '%'"
:trend="statsData.orders.trend > 0 ? 'up' : 'down'"
footerLeftText="本月订单量"
:footerRightText="statsData.orders.monthTotal + '单'"
/>
<!-- 新增用户卡片 -->
<view class="stat-card">
<view class="card-header">
<text class="card-title">新增用户</text>
<view class="tag today">今日</view>
</view>
<view class="card-value">21</view>
<view class="card-meta">
<text class="meta-text">昨日 6</text>
<text class="meta-trend up">日环比 250% ▲</text>
</view>
<view class="card-footer">
<text class="footer-label">本月新增用户</text>
<text class="footer-value">27人</text>
</view>
</view>
<KpiMiniCard
class="stat-card"
title="新增用户"
tagText="今日"
:valueText="statsData.users.today.toString()"
:metaLeft="'昨日 ' + statsData.users.yesterday"
:metaRight="'日环比 ' + statsData.users.trend + '%'"
:trend="statsData.users.trend > 0 ? 'up' : 'down'"
footerLeftText="本月新增用户"
:footerRightText="statsData.users.monthTotal + '人'"
/>
</view>
<!-- 订单趋势图表区 -->
@@ -156,6 +136,7 @@ import { ref, computed, onMounted } from 'vue'
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
import AnalyticsAreaChart from '@/components/analytics/AnalyticsAreaChart.uvue'
import AnalyticsPieChart from '@/components/analytics/AnalyticsPieChart.uvue'
import KpiMiniCard from '@/pages/mall/admin/homePage/components/KpiMiniCard.uvue'
// Filter periods
const periods = [
@@ -249,90 +230,9 @@ const statsData = ref({
min-height: 100vh;
}
.stats-row {
display: flex;
flex-direction: row;
gap: 16px;
flex-wrap: wrap;
}
/* 统计卡片 */
/* 兼容旧布局标识,样式逻辑已由 .kpi-grid 接管 */
.stat-card {
flex: 1;
min-width: 280px;
background-color: #ffffff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
}
.stat-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* 卡片头部 */
.card-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.card-title {
font-size: 14px;
color: #666666;
font-weight: 400;
}
.tag {
padding: 2px 8px;
border-radius: 2px;
font-size: 12px;
}
.tag.today {
background-color: #e8f4ff;
color: #1890ff;
}
/* 卡片主值 */
.card-value {
font-size: 32px;
font-weight: 500;
color: #262626;
margin-bottom: 12px;
line-height: 1.2;
}
/* 卡片元数据 */
.card-meta {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.meta-text {
font-size: 13px;
color: #8c8c8c;
}
.meta-trend {
font-size: 13px;
font-weight: 500;
}
.meta-trend.up {
color: #ff4d4f;
}
.meta-trend.down {
color: #52c41a;
margin-bottom: 0px;
}
/* 图表区样式 */
@@ -453,14 +353,11 @@ const statsData = ref({
font-weight: 500;
}
/* 响应式 */
@media screen and (max-width: 1400px) {
.stat-card {
min-width: calc(50% - 8px);
}
@media screen and (max-width: 768px) {
.home-page {
padding: 12px;
}
@media screen and (max-width: 1024px) {
.bottom-charts {
flex-direction: column;
}
@@ -468,24 +365,25 @@ const statsData = ref({
.half-width {
min-width: 100%;
}
}
@media screen and (max-width: 768px) {
.home-page {
padding: 12px;
}
.stats-row {
/* 调整图表头部在移动端的展示 */
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.stat-card {
min-width: 100%;
padding: 16px;
.header-right {
width: 100%;
}
.card-value {
font-size: 28px;
.period-tabs {
width: 100%;
justify-content: space-between;
}
.period-tab {
flex: 1;
}
}
</style>

View File

@@ -38,8 +38,18 @@ export const tabs = ref<TabItem[]>([])
/** 是否折叠主侧边栏 */
export const isMainAsideCollapsed = ref<boolean>(false)
/** 屏幕宽度 */
export const windowWidth = ref<number>(1024)
/** 是否为移动端布局 (width < 768) */
export const isMobile = computed<boolean>(() => windowWidth.value < 768)
/** 移动端菜单是否展开 */
export const isMobileMenuOpen = ref<boolean>(false)
/** 是否显示二级侧边栏 */
export const showSubSider = computed<boolean>(() => {
if (isMobile.value) return false // 移动端不显式二级侧边栏在主体区域
const topMenus = getTopMenus()
const activeMenu = topMenus.find(m => m.id === activeTopMenuId.value)
return activeMenu ? activeMenu.groups.length > 0 : false

View File

@@ -0,0 +1,36 @@
/* 统一 KPI 统计网格响应式规范 */
.kpi-grid {
display: grid !important;
gap: 16px;
width: 100%;
box-sizing: border-box;
}
/* 规则 1屏幕宽度 >= 1200px -> 固定 4 列 */
@media (min-width: 1200px) {
.kpi-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
}
}
/* 规则 2768px <= 屏幕宽度 < 1200px -> 固定 2 列 */
@media (min-width: 768px) and (max-width: 1199.98px) {
.kpi-grid {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}
}
/* 规则 3屏幕宽度 < 768px -> 固定 1 列 */
@media (max-width: 767.98px) {
.kpi-grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
}
}
/* 强制子项允许收缩,防止内部长文本撑爆网格 */
.kpi-grid > * {
min-width: 0 !important;
flex: none !important; /* 覆盖旧的 flex 逻辑 */
width: auto !important; /* 让 grid 控制宽度 */
}

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() {