保留页面布局

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

@@ -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">
<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>
<!-- 移动端菜单切换按钮 (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 控制宽度 */
}