ylj20011123 c78652a8d1 update
2025-10-23 18:35:54 +08:00

693 lines
23 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>
<view class="digital-dashboard">
<!-- Tab切换区域 -->
<view class="tab-container">
<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>
</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, index) 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 class="content-container">
<!-- 经营数据分析 -->
<view v-if="activeTab === 0" class="tab-content">
<!-- 商品报表及分析 -->
<view id="product-report"> </view>
<ProductReport />
<!-- 交易报表及分析 -->
<view id="transaction-report"></view>
<TransactionReport />
<!-- 会员报表及分析 -->
<view id="member-report"></view>
<MemberReport />
<!-- 店铺报表及分析 -->
<view id="shop-report"></view>
<ShopReport />
</view>
<!-- 供应链数据分析 -->
<view v-if="activeTab === 1" class="tab-content">
<!-- 库存结构分析 -->
<view id="inventory-structure"></view>
<InventoryStructureAnalysis />
<!-- 库存新鲜度分析 -->
<view id="inventory-freshness"> </view>
<InventoryFreshnessAnalysis />
<!-- 库存精准度分析 -->
<view id="inventory-accuracy"> </view>
<InventoryAccuracyAnalysis />
<!-- 库存周转率分析 -->
<view id="inventory-turnover"></view>
<InventoryTurnoverAnalysis />
<!-- 商品毛利率分析 -->
<view id="gross-margin"> </view>
<GrossMarginAnalysis />
</view>
<!-- 资金数据分析 -->
<view v-if="activeTab === 2" class="tab-content">
<!-- 订单和交易数据交叉分析 -->
<view id="order-transaction"></view>
<OrderTransactionAnalysis />
<!-- 订单和结算数据交叉分析 -->
<view id="order-settlement"></view>
<OrderSettlementAnalysis />
<!-- 平台和服务商及店铺分账数据交叉分析 -->
<view id="revenue-share"> </view>
<RevenueShareAnalysis />
<!-- 订单和退单数据交叉分析 -->
<view id="order-refund"></view>
<OrderRefundAnalysis />
</view>
<!-- 综合运营分析 -->
<view v-if="activeTab === 3" class="tab-content">
<!-- 按路线的服务区经营状态分析 -->
<view id="route-service-area"> </view>
<RouteServiceAreaAnalysis />
<!-- 按行政区域的服务区经营状态分析 -->
<view id="regional-service-area"></view>
<RegionalServiceAreaAnalysis />
<!-- 全省服务区经营数据状态分析 -->
<view id="province-service-area"> </view>
<ProvinceServiceAreaAnalysis />
<!-- 按管理方自定义服务区等级的经营状态分析 -->
<view id="custom-service-area"></view>
<CustomServiceAreaAnalysis />
</view>
<!-- 深度数据分析 -->
<view v-if="activeTab === 4" class="tab-content">
<!-- 自定义数据分析组件 -->
<view id="custom-data-analysis"></view>
<CustomDataAnalysis />
</view>
</view>
</view>
</template>
<script>
import ProductReport from './components/ProductReport.vue'
import TransactionReport from './components/TransactionReport.vue'
import MemberReport from './components/MemberReport.vue'
import ShopReport from './components/ShopReport.vue'
import InventoryStructureAnalysis from './components/InventoryStructureAnalysis.vue'
import InventoryFreshnessAnalysis from './components/InventoryFreshnessAnalysis.vue'
import InventoryAccuracyAnalysis from './components/InventoryAccuracyAnalysis.vue'
import InventoryTurnoverAnalysis from './components/InventoryTurnoverAnalysis.vue'
import GrossMarginAnalysis from './components/GrossMarginAnalysis.vue'
import OrderTransactionAnalysis from './components/OrderTransactionAnalysis.vue'
import OrderSettlementAnalysis from './components/OrderSettlementAnalysis.vue'
import RevenueShareAnalysis from './components/RevenueShareAnalysis.vue'
import OrderRefundAnalysis from './components/OrderRefundAnalysis.vue'
import CustomDataAnalysis from './components/CustomDataAnalysis.vue'
import RouteServiceAreaAnalysis from './components/RouteServiceAreaAnalysis.vue'
import RegionalServiceAreaAnalysis from './components/RegionalServiceAreaAnalysis.vue'
import ProvinceServiceAreaAnalysis from './components/ProvinceServiceAreaAnalysis.vue'
import CustomServiceAreaAnalysis from './components/CustomServiceAreaAnalysis.vue'
export default {
components: {
ProductReport,
TransactionReport,
MemberReport,
ShopReport,
InventoryStructureAnalysis,
InventoryFreshnessAnalysis,
InventoryAccuracyAnalysis,
InventoryTurnoverAnalysis,
GrossMarginAnalysis,
OrderTransactionAnalysis,
OrderSettlementAnalysis,
RevenueShareAnalysis,
OrderRefundAnalysis,
CustomDataAnalysis,
RouteServiceAreaAnalysis,
RegionalServiceAreaAnalysis,
ProvinceServiceAreaAnalysis,
CustomServiceAreaAnalysis
},
data() {
return {
activeTab: 0,
tabList: [
{ name: '经营数据分析', key: 'business' },
{ name: '供应链数据分析', key: 'supply' },
{ name: '资金数据分析', key: 'finance' },
{ name: '综合运营分析', key: 'operation' },
{ name: '深度数据分析', key: 'deep-analysis' }
],
// 各个Tab对应的导航栏数据
navData: {
business: [
{ id: 'product-report', name: '商品报表及分析' },
{ id: 'transaction-report', name: '交易报表及分析' },
{ id: 'member-report', name: '会员报表及分析' },
{ id: 'shop-report', name: '店铺报表及分析' }
],
supply: [
{ id: 'inventory-structure', name: '库存结构分析' },
{ id: 'inventory-freshness', name: '库存新鲜度分析' },
{ id: 'inventory-accuracy', name: '库存精准度分析' },
{ id: 'inventory-turnover', name: '库存周转率分析' },
{ id: 'gross-margin', name: '商品毛利率分析' }
],
finance: [
{ id: 'order-transaction', name: '订单和交易数据交叉分析' },
{ id: 'order-settlement', name: '订单和结算数据交叉分析' },
{ id: 'revenue-share', name: '平台和服务商及店铺分账数据交叉分析' },
{ id: 'order-refund', name: '订单和退单数据交叉分析' }
],
operation: [
{ id: 'route-service-area', name: '按路线的服务区经营状态分析' },
{ id: 'regional-service-area', name: '按行政区域的服务区经营状态分析' },
{ id: 'province-service-area', name: '全省服务区经营数据状态分析' },
{ id: 'custom-service-area', name: '按管理方自定义服务区等级的经营状态分析' }
],
'deep-analysis': [
{ id: 'custom-data-analysis', name: '自定义数据分析' }
]
},
// 当前活动的导航项
activeNavItem: '',
// 导航栏是否收缩
isNavCollapsed: true,
// 暂时将这一次进入 的数据缓存一下
sessionData: {}
}
},
computed: {
// 获取当前Tab的导航项
currentNavItems() {
const currentTabKey = this.tabList[this.activeTab].key;
return this.navData[currentTabKey] || [];
}
},
onLoad() {
this.fetchTabData(this.activeTab);
},
onShow() {
// 隐藏小程序原生tabbar
uni.hideTabBar();
},
onPageScroll() {
// 监听页面滚动
this.handleScroll();
},
onReady() {
// 初始化活动导航项
const currentTabKey = this.tabList[this.activeTab].key;
const navItems = this.navData[currentTabKey] || [];
if (navItems.length > 0) {
this.activeNavItem = navItems[0].id;
}
},
methods: {
switchTab(index) {
this.activeTab = index;
// 模拟数据加载
this.fetchTabData(index);
// 等待DOM更新后滚动到顶部
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 0,
duration: 300
});
});
},
// 滚动到指定组件
scrollToComponent(componentId) {
this.activeNavItem = componentId;
setTimeout(() => {
uni.createSelectorQuery().select('#' + componentId).boundingClientRect((data) => {
if (data) {
uni.createSelectorQuery().selectViewport().scrollOffset((scrollData) => {
const targetScrollTop = scrollData.scrollTop + data.top - 80;
uni.pageScrollTo({
scrollTop: Math.max(0, targetScrollTop),
duration: 300
});
}).exec();
}
}).exec();
}, 100);
},
// 切换导航栏收缩状态
toggleNavCollapse() {
this.isNavCollapsed = !this.isNavCollapsed;
},
// 监听页面滚动,更新活动导航项
handleScroll() {
// 简化处理避免频繁查询DOM
},
// 获取各个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 {
min-height: 100vh;
background: linear-gradient(180deg, @bg-light 0%, #e9ecf4 100%);
.tab-container {
background: @bg-white;
box-shadow: @shadow-light;
position: sticky;
top: 0;
z-index: 100;
.tab-list {
display: flex;
padding: 0 32rpx;
box-sizing: border-box;
position: relative;
width: 100%;
overflow-x: auto;
scrollbar-width: none;
/* 隐藏滚动条 Firefox */
-ms-overflow-style: none;
/* 隐藏滚动条 IE/Edge */
&::-webkit-scrollbar {
display: none;
/* 隐藏滚动条 Chrome/Safari */
}
.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;
&:last-child {
margin-right: 0;
}
&.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;
.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;
}
}
}
}
}
}
}
.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>