ylj20011123 d832cfa05d update
2025-10-30 11:38:09 +08:00

737 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<page-meta :page-style="'overflow-x:hidden'"></page-meta>
<scroll-view scroll-y @scroll="handleScroll" class="digital-dashboard" :scroll-into-view="scrollIntoView"
:scroll-with-animation="true">
<!-- Tab切换区域 -->
<scroll-view scroll-x class="tab-container" :scroll-with-animation="true" :scroll-left="tabScrollPosition"
show-scrollbar="false">
<view class="tab-list">
<view v-for="(tab, index) in tabList" :key="index" :id="`tab-${index}`" class="tab-item"
:class="{ active: activeTab === index }" @click="switchTab(index)">
<text class="tab-text">{{ tab.name }}</text>
<view class="tab-indicator" v-if="activeTab === index"></view>
</view>
</view>
</scroll-view>
<!-- 右侧悬浮导航栏 -->
<view class="side-navigation" v-if="currentNavItems && currentNavItems.length > 0"
:class="{ collapsed: isNavCollapsed }">
<view class="nav-container">
<view class="nav-header" @click="toggleNavCollapse">
<view class="nav-title" v-if="!isNavCollapsed">快速导航</view>
<view class="nav-toggle">
<text class="toggle-icon" :class="{ rotated: isNavCollapsed }"></text>
</view>
</view>
<view class="nav-list" v-show="!isNavCollapsed">
<view v-for="item in currentNavItems" :key="item.id" class="nav-item"
:class="{ active: activeNavItem === item.id }" @click="scrollToComponent(item.id)">
<text class="nav-text">{{ item.name }}</text>
<view class="nav-dot" v-if="activeNavItem === item.id"></view>
</view>
</view>
</view>
</view>
<view id="top"> </view>
<!-- 内容展示区域 -->
<view class="content-container">
<!-- 时间选择 -->
<view class="timeBox">
<picker mode="date" @change="bindDateChange" :value="selectTime" fields="month">
统计时间{{ selectTime }}
</picker>
</view>
<!-- 实时运营监控中心 -->
<view v-if="activeTab === 0" class="tab-content">
<!-- 服务区概况 -->
<view id="overview-of-serviceArea"></view>
<OverviewOfServiceArea />
<!-- 交易预警 -->
<view id="trading-alert"></view>
<TradingAlert />
<!-- 断面流量 -->
<view id="trend-of-trafficFlow"></view>
<TrendOfTrafficFlow :selectTime="selectTime" />
<!-- 入区车流 -->
<view id="vehicles-entering"></view>
<VehiclesEntering :selectTime="selectTime" />
<!-- 经营效益 -->
<view id="vehicle-model-stay"></view>
<VehicleModelStay :selectTime="selectTime" />
</view>
<!-- 客群画像与消费行为分析 -->
<view v-if="activeTab === 1" class="tab-content">
<!-- 年龄画像 -->
<view id="customer-age-group"></view>
<CustomerAgeGroup :selectTime="selectTime" />
<!-- 性别画像 -->
<view id="gender-customer-group"></view>
<GenderCustomerGroup :selectTime="selectTime" />
<!-- 偏好类型 -->
<view id="preference-type"></view>
<PreferenceType :selectTime="selectTime" />
<!-- 客群特征分析 -->
<view id="customer-group"></view>
<CustomerGroup :selectTime="selectTime" />
<!-- 客群消费偏好 -->
<view id="customer-consumption-preferences"></view>
<CustomerConsumptionPreferences :selectTime="selectTime" />
<!-- 消费转化率对比图 -->
<view id="consumption-conversion"></view>
<ConsumptionConversion :selectTime="selectTime" />
<!-- 消费水平 -->
<view id="consumption-level"></view>
<ConsumptionLevel :selectTime="selectTime" />
<!-- 消费时段分析 -->
<view id="consumption-period"></view>
<ConsumptionPeriod :selectTime="selectTime" />
<!-- 品牌消费水平 -->
<view id="brand-consumption-level"></view>
<BrandConsumptionLevel :selectTime="selectTime" />
</view>
<!-- 多维度经营数据分析 -->
<view v-if="activeTab === 2" class="tab-content">
<!-- 营收特征 -->
<view id="business-case"></view>
<BusinessCase :selectTime="selectTime" />
<!-- 区域营收占比 -->
<view id="regional-revenue"></view>
<RegionalRevenue :selectTime="selectTime" />
<!-- 业态结构占比 -->
<view id="business-structure"></view>
<BusinessStructure :selectTime="selectTime" />
<!-- 节假日营收 -->
<view id="festival-revenue-sum-info"></view>
<FestivalRevenueSumInfo />
</view>
<!-- 商户电商生态全景 -->
<view v-if="activeTab === 3" class="tab-content">
<!-- 会员商城 -->
<view id="member-mall"></view>
<MemberMall :selectTime="selectTime" />
<!-- 热门商品榜单 -->
<view id="hot-product-list"></view>
<HotProductList />
<!-- 商户类别 -->
<view id="brand-detail"></view>
<BrandDetail />
<!-- 供应商列表 -->
<view id="supplier-list-box"></view>
<SupplierListBox :selectTime="selectTime" />
<!-- 商城订单统计 -->
<view id="mall-order-statistics"></view>
<MallOrderStatistics :selectTime="selectTime" />
<!-- 本月福利金发送额度 -->
<view id="this-month-benefits"></view>
<ThisMonthBenefits :selectTime="selectTime" />
<!-- 会员消费数据分析 -->
<view id="analysis-of-member"></view>
<AnalysisOfMember />
</view>
</view>
</scroll-view>
</template>
<script>
import OverviewOfServiceArea from './components/OverviewOfServiceArea.vue'
import TradingAlert from './components/TradingAlert.vue'
import TrendOfTrafficFlow from './components/TrendOfTrafficFlow.vue'
import VehiclesEntering from './components/VehiclesEntering.vue'
import VehicleModelStay from './components/VehicleModelStay.vue'
import CustomerAgeGroup from './components/CustomerAgeGroup.vue'
import GenderCustomerGroup from './components/GenderCustomerGroup.vue'
import PreferenceType from './components/PreferenceType.vue'
import CustomerGroup from './components/CustomerGroup.vue'
import CustomerConsumptionPreferences from './components/CustomerConsumptionPreferences.vue'
import ConsumptionConversion from './components/ConsumptionConversion.vue'
import ConsumptionLevel from './components/ConsumptionLevel.vue'
import ConsumptionPeriod from './components/ConsumptionPeriod.vue'
import BrandConsumptionLevel from './components/BrandConsumptionLevel.vue'
import BusinessCase from './components/BusinessCase.vue'
import RegionalRevenue from './components/RegionalRevenue.vue'
import BusinessStructure from './components/BusinessStructure.vue'
import FestivalRevenueSumInfo from './components/FestivalRevenueSumInfo.vue'
import MemberMall from './components/MemberMall.vue'
import HotProductList from './components/HotProductList.vue'
import BrandDetail from './components/BrandDetail.vue'
import SupplierListBox from './components/SupplierListBox.vue'
import MallOrderStatistics from './components/MallOrderStatistics.vue'
import ThisMonthBenefits from './components/ThisMonthBenefits.vue'
import AnalysisOfMember from './components/AnalysisOfMember.vue'
import moment from 'moment'
export default {
components: {
OverviewOfServiceArea,
TradingAlert,
TrendOfTrafficFlow,
VehiclesEntering,
VehicleModelStay,
CustomerAgeGroup,
GenderCustomerGroup,
PreferenceType,
CustomerGroup,
CustomerConsumptionPreferences,
ConsumptionConversion,
ConsumptionLevel,
ConsumptionPeriod,
BrandConsumptionLevel,
BusinessCase,
RegionalRevenue,
BusinessStructure,
FestivalRevenueSumInfo,
MemberMall,
HotProductList,
BrandDetail,
SupplierListBox,
MallOrderStatistics,
ThisMonthBenefits,
AnalysisOfMember
},
data() {
return {
activeTab: 0,
tabList: [
{ name: '运营中心', key: 'business' },
{ name: '客群画像', key: 'customerProfile' },
{ name: '经营分析', key: 'businessRevenue' },
{ name: '电商生态', key: 'mallOperation' },
],
// 各个Tab对应的导航栏数据
navData: {
business: [
{ id: 'overview-of-serviceArea', name: '服务区概况' },
{ id: 'trading-alert', name: '交易预警' },
{ id: 'trend-of-trafficFlow', name: '断面流量' },
{ id: 'vehicles-entering', name: '入区车流' },
{ id: 'vehicle-model-stay', name: '经营效益' },
],
customerProfile: [
{ id: 'customer-age-group', name: '年龄画像' },
{ id: 'gender-customer-group', name: '性别画像' },
{ id: 'preference-type', name: '偏好类型' },
{ id: 'customer-group', name: '客群特征' },
{ id: 'customer-consumption-preferences', name: '消费偏好' },
{ id: 'consumption-conversion', name: '消费转化率' },
{ id: 'consumption-level', name: '消费水平' },
{ id: 'consumption-period', name: '消费时段' },
{ id: 'brand-consumption-level', name: '品牌消费' },
],
businessRevenue: [
{ id: 'business-case', name: '营收特征' },
{ id: 'regional-revenue', name: '区域营收' },
{ id: 'business-structure', name: '业态结构' },
{ id: 'festival-revenue-sum-info', name: '节假日营收' },
],
mallOperation: [
{ id: 'member-mall', name: '会员商城' },
{ id: 'hot-product-list', name: '商品榜单' },
{ id: 'brand-detail', name: '商户类别' },
{ id: 'supplier-list-box', name: '供应商列表' },
{ id: 'mall-order-statistics', name: '商城订单统计' },
{ id: 'this-month-benefits', name: '福利金额度' },
{ id: 'analysis-of-member', name: '会员消费' },
]
},
// 当前活动的导航项
activeNavItem: '',
// 导航栏是否收缩
isNavCollapsed: false,
// 暂时将这一次进入 的数据缓存一下
sessionData: {},
pageScrollTop: 0,
scrollIntoView: "",
tabScrollPosition: 0,
selectTime: moment().subtract(1, 'M').format('YYYY-MM')
}
},
computed: {
// 获取当前Tab的导航项
currentNavItems() {
const currentTabKey = this.tabList[this.activeTab].key;
return this.navData[currentTabKey] || [];
}
},
onLoad() {
this.fetchTabData(this.activeTab);
},
onShow() {
// 隐藏小程序原生tabbar
uni.hideTabBar();
},
onReady() {
// 初始化活动导航项
const currentTabKey = this.tabList[this.activeTab].key;
const navItems = this.navData[currentTabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
},
methods: {
bindDateChange(e) {
console.log('eeeee', e);
this.selectTime = e.detail.value
},
switchTab(index) {
this.scrollIntoView = 'top'
this.activeTab = index;
// 模拟数据加载
this.fetchTabData(index);
// 使用固定步长来计算滚动位置
// 假设每个Tab的平均宽度为 200rpx (100px)
const avgTabWidth = 100;
const targetPosition = index * avgTabWidth;
console.log('Switching to tab:', index, 'scrolling to:', targetPosition);
// 设置滚动位置
this.tabScrollPosition = targetPosition;
},
// 滚动到指定组件
scrollToComponent(componentId) {
this.scrollIntoView = componentId
this.activeNavItem = componentId;
},
// 切换导航栏收缩状态
toggleNavCollapse() {
this.isNavCollapsed = !this.isNavCollapsed;
},
handleScroll(e) {
this.pageScrollTop = e.detail.scrollTop
},
// 获取各个tab的数据
async fetchTabData(tabIndex) {
// 切换Tab时重置活动导航项
const currentTabKey = this.tabList[tabIndex].key;
const navItems = this.navData[currentTabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
}
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@success-color: #52c41a;
@warning-color: #faad14;
@error-color: #ff4d4f;
@text-primary: #333;
@text-secondary: #666;
@text-light: #999;
@bg-light: #f8f9fb;
@bg-white: #ffffff;
@border-radius: 16rpx;
@shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
@shadow-light: 0 2px 12px rgba(0, 0, 0, 0.06);
.digital-dashboard {
height: 100vh;
background: linear-gradient(180deg, @bg-light 0%, #e9ecf4 100%);
.tab-container {
background: @bg-white;
box-shadow: @shadow-light;
position: fixed;
top: 0;
z-index: 100;
width: 100%;
.tab-list {
height: 80rpx;
display: flex;
padding: 0 32rpx;
box-sizing: border-box;
position: relative;
width: max-content;
white-space: nowrap;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
color: transparent;
}
.tab-item {
position: relative;
padding: 16rpx 24rpx;
margin-right: 16rpx;
flex-shrink: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
min-width: fit-content;
display: inline-block;
&.active {
.tab-text {
color: @primary-color;
font-weight: 600;
transform: translateY(-1px);
}
.tab-indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 3rpx;
animation: slideIn 0.3s ease-out;
}
}
.tab-text {
font-size: 28rpx;
color: @text-secondary;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -4rpx;
left: 0;
width: 100%;
height: 2rpx;
background: @primary-color;
transform: scaleX(0);
transition: transform 0.3s ease;
}
}
&:active {
transform: scale(0.95);
}
}
}
}
.content-container {
padding: 32rpx;
padding-top: 92rpx;
.tab-content {
/* 移除切换动画 */
.chart-grid {
.chart-row {
margin-bottom: 24rpx;
.chart-card {
background: @bg-white;
border-radius: @border-radius;
padding: 24rpx;
box-shadow: @shadow;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
box-sizing: border-box;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, @primary-color, @secondary-color);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active {
transform: translateY(-2rpx);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
&::before {
opacity: 1;
}
}
&.full {
width: 100%;
min-height: 400rpx;
}
.chart-header {
margin-bottom: 20rpx;
.chart-title {
font-size: 28rpx;
font-weight: 600;
color: @text-primary;
margin-bottom: 4rpx;
}
.chart-subtitle {
font-size: 22rpx;
color: @text-light;
}
}
.chart-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 12rpx;
position: relative;
overflow: hidden;
&.large {
height: 400rpx;
}
&::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(103, 126, 234, 0.1) 0%, transparent 70%);
animation: rotate 30s infinite linear;
}
.placeholder-text {
font-size: 32rpx;
color: @text-secondary;
margin-bottom: 8rpx;
position: relative;
z-index: 1;
}
.placeholder-desc {
font-size: 24rpx;
color: @text-light;
position: relative;
z-index: 1;
}
}
}
}
}
}
}
.timeBox {
width: 100%;
display: flex;
justify-content: flex-end;
}
.side-navigation {
position: fixed;
right: 24rpx;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 16rpx;
box-shadow: @shadow;
border: 1rpx solid rgba(255, 255, 255, 0.2);
max-height: 70vh;
overflow-y: auto;
transition: all 0.3s ease;
&::-webkit-scrollbar {
width: 4rpx;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
border-radius: 2rpx;
}
&::-webkit-scrollbar-thumb {
background: @primary-color;
border-radius: 2rpx;
}
// 收缩状态
&.collapsed {
min-width: auto;
max-width: auto;
.nav-container {
padding: 0;
}
}
.nav-container {
padding: 0 16rpx;
.nav-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
margin-bottom: 8rpx;
cursor: pointer;
.nav-title {
font-size: 24rpx;
font-weight: 600;
color: @text-primary;
}
.nav-toggle {
.toggle-icon {
font-size: 20rpx;
color: @text-secondary;
transition: transform 0.3s ease;
&.rotated {
transform: rotate(180deg);
}
}
}
&:hover {
background: rgba(102, 126, 234, 0.05);
border-radius: 8rpx;
}
}
.nav-list {
.nav-item {
position: relative;
padding: 16rpx 20rpx;
margin-bottom: 8rpx;
border-radius: 12rpx;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background: rgba(102, 126, 234, 0.1);
transform: translateX(-4rpx);
}
&.active {
background: linear-gradient(90deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.15));
border-left: 4rpx solid @primary-color;
padding-left: 16rpx;
.nav-text {
color: @primary-color;
font-weight: 600;
}
}
.nav-text {
font-size: 22rpx;
color: @text-secondary;
line-height: 1.4;
flex: 1;
transition: all 0.3s ease;
}
.nav-dot {
width: 8rpx;
height: 8rpx;
background: @primary-color;
border-radius: 50%;
animation: pulse 2s infinite;
}
}
}
}
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.2);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideIn {
from {
width: 0;
opacity: 0;
}
to {
width: 48rpx;
opacity: 1;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>