首页细节调整

This commit is contained in:
2026-02-06 16:18:04 +08:00
parent d00f0b7412
commit 57846534bc
86 changed files with 2751 additions and 3074 deletions

View File

@@ -1,8 +1,8 @@
<template>
<template>
<view class="page" @click.self="closeMoreMenu">
<!-- 鍥哄畾椤堕儴瀵艰埅鏍?-->
<!-- 固定顶部导航栏 -->
<AnalyticsTopBar
:title="'鏁版嵁鍒嗘瀽涓績'"
:title="'数据分析中心'"
:lastUpdateTime="lastUpdateTime"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@@ -16,25 +16,25 @@
/>
<view class="page-layout">
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<!-- 侧边栏菜单组件 -->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 涓诲唴瀹瑰尯鍩?-->
<!-- 主内容区域 -->
<view class="main-content">
<view class="container">
<!-- KPI锛氬灞?4鍒楋紝绐勫睆 2鍒楋紙澧炲己鐗堬細娓愬彉鑳屾櫙 + sparkline锛?-->
<!-- KPI:宽屏 4列窄屏 2列增强版渐变背景 + sparkline -->
<view class="kpi-grid">
<view class="kpi-card kpi-card-gmv" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">瀹炴椂 GMV</text>
<text class="kpi-label">实时 GMV</text>
</view>
<text class="kpi-value">{{ formatMoney(realTime.gmv) }}</text>
<text class="kpi-value">¥{{ formatMoney(realTime.gmv) }}</text>
<view class="kpi-footer">
<text class="kpi-meta">杈冩槰鏃ュ悓鍒?/text>
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.gmv_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.gmv_growth) }}
</text>
@@ -42,11 +42,11 @@
</view>
<view class="kpi-card kpi-card-orders" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">瀹炴椂璁㈠崟</text>
<text class="kpi-label">实时订单</text>
</view>
<text class="kpi-value">{{ formatInt(realTime.orders) }}</text>
<view class="kpi-footer">
<text class="kpi-meta">杈冩槰鏃ュ悓鍒?/text>
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.order_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.order_growth) }}
</text>
@@ -54,21 +54,21 @@
</view>
<view class="kpi-card kpi-card-users" @click="goToUserAnalysis">
<view class="kpi-header">
<text class="kpi-label">鍦ㄧ嚎鐢ㄦ埛</text>
<text class="kpi-label">在线用户</text>
</view>
<text class="kpi-value">{{ formatInt(realTime.online_users) }}</text>
<view class="kpi-footer">
<text class="kpi-meta">鏈€杩?5 鍒嗛挓</text>
<text class="kpi-chip neutral">瀹炴椂</text>
<text class="kpi-meta">最近 5 分钟</text>
<text class="kpi-chip neutral">实时</text>
</view>
</view>
<view class="kpi-card kpi-card-conversion" @click="goToSalesReport">
<view class="kpi-header">
<text class="kpi-label">杞寲鐜?/text>
<text class="kpi-label">转化率</text>
</view>
<text class="kpi-value">{{ formatPct(realTime.conversion_rate) }}</text>
<view class="kpi-footer">
<text class="kpi-meta">杈冩槰鏃ュ悓鍒?/text>
<text class="kpi-meta">较昨日同刻</text>
<text class="kpi-chip" :class="realTime.conversion_growth >= 0 ? 'pos' : 'neg'">
{{ formatPct(realTime.conversion_growth) }}
</text>
@@ -76,7 +76,7 @@
</view>
</view>
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<!-- 时间维度筛选(快捷 + 自定义) -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -92,7 +92,8 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
鑷畾涔? </view>
自定义
</view>
</view>
<AnalyticsDateRangePicker
@@ -103,15 +104,15 @@
@clear="onDateRangeClear"
/>
<!-- 鏍稿績瓒嬪娍锛氬崰婊℃í鍚戯紙鏌?鎶?缁勫悎鍥撅級 -->
<!-- 核心趋势:占满横向(柱+折 组合图) -->
<view class="card card-full">
<view class="card-head">
<text class="card-title">鏍稿績瓒嬪娍锛圙MV / 璁㈠崟鏁帮級</text>
<text class="card-desc">{{ selectedPeriodText }} 路 鏌憋細GMV锛堝厓锛?路 绾匡細璁㈠崟鏁?/text>
<text class="card-title">核心趋势GMV / 订单数)</text>
<text class="card-desc">{{ selectedPeriodText }} · 柱GMV · 线:订单数</text>
</view>
<view v-if="loading || !trend.x || trend.x.length === 0" class="chart-loading">
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
<view v-else style="width: 100%; height: 320px; position: relative; overflow: hidden; min-height: 320px;">
<AnalyticsComboChart
@@ -124,16 +125,16 @@
</view>
</view>
<!-- 鐢ㄦ埛缁撴瀯鍜屾祦閲忔潵婧愶細妯帓鏄剧ず -->
<!-- 用户结构和流量来源:横排显示 -->
<view class="charts-row">
<!-- 宸︿晶锛氱敤鎴风粨鏋?-->
<!-- 左侧:用户结构 -->
<view class="charts-left card">
<view class="card-head">
<text class="card-title">鐢ㄦ埛缁撴瀯锛堢幆褰㈠浘锛?/text>
<text class="card-desc">鏈秷璐?/ 棣栬喘 / 澶嶈喘 / 鍥炴祦</text>
<text class="card-title">用户结构(环形图)</text>
<text class="card-desc">未消费 / 首购 / 复购 / 回流</text>
</view>
<view v-if="loading || !userSegmentOption || !userSegmentOption.series || (userSegmentOption.series && userSegmentOption.series.length === 0)" class="chart-loading">
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
<view v-else class="chart-box">
<EChartsView
@@ -143,14 +144,14 @@
</view>
</view>
<!-- 鍙充晶锛氭祦閲忔潵婧?-->
<!-- 右侧:流量来源 -->
<view class="charts-right card">
<view class="card-head">
<text class="card-title">娴侀噺鏉ユ簮锛堟潯褰級</text>
<text class="card-desc">鍗犳瘮%</text>
<text class="card-title">流量来源(条形)</text>
<text class="card-desc">占比%</text>
</view>
<view v-if="loading || !trafficBarOption || !trafficBarOption.series || trafficBarOption.series.length === 0" class="chart-loading">
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
<view v-else class="chart-box">
<EChartsView
@@ -161,13 +162,13 @@
</view>
</view>
<!-- 涓や釜TOP鎺掕锛氭í鎺掓樉绀?-->
<!-- 两个TOP排行横排显示 -->
<view class="tops-row">
<!-- 宸︿晶锛氱儹閿€鍟嗗搧TOP -->
<!-- 左侧:热销商品TOP -->
<view class="tops-left card">
<view class="card-head">
<text class="card-title">鐑攢鍟嗗搧 TOP</text>
<text class="card-desc">鎸夐攢閲?/text>
<text class="card-title">热销商品 TOP</text>
<text class="card-desc">按销量</text>
</view>
<view class="rank-scroll-container">
<view class="rank-scroll-wrapper" :class="{ 'has-scroll': topProducts.length >= 6 }">
@@ -175,24 +176,24 @@
<view v-for="p in topProducts" :key="p.id" class="rank-item">
<text class="rank-no">{{ p.rank }}</text>
<text class="rank-name">{{ p.name }}</text>
<text class="rank-val">{{ p.sales }} 浠?/text>
<text class="rank-val">{{ p.sales }} 件</text>
</view>
<!-- 寰幆鎾斁锛氬鍒朵竴浠芥暟鎹敤浜庢棤缂濇粴鍔?-->
<!-- 循环播放:复制一份数据用于无缝滚动 -->
<view v-if="topProducts.length >= 6" v-for="p in topProducts" :key="'copy-' + p.id" class="rank-item">
<text class="rank-no">{{ p.rank }}</text>
<text class="rank-name">{{ p.name }}</text>
<text class="rank-val">{{ p.sales }} 浠?/text>
<text class="rank-val">{{ p.sales }} 件</text>
</view>
</view>
</view>
</view>
</view>
<!-- 鍙充晶锛氬晢瀹舵帓琛孴OP -->
<!-- 右侧商家排行TOP -->
<view class="tops-right card">
<view class="card-head">
<text class="card-title">鍟嗗鎺掕 TOP</text>
<text class="card-desc">鎸?GMV</text>
<text class="card-title">商家排行 TOP</text>
<text class="card-desc">GMV</text>
</view>
<view class="rank-scroll-container">
<view class="rank-scroll-wrapper" :class="{ 'has-scroll': topMerchants.length >= 6 }">
@@ -201,18 +202,18 @@
<text class="rank-no">{{ m.rank }}</text>
<text class="rank-name">{{ m.name }}</text>
<view class="rank-right">
<text class="rank-val">{{ formatMoney(m.sales) }}</text>
<text class="rank-val">¥{{ formatMoney(m.sales) }}</text>
<text class="chip" :class="m.growth >= 0 ? 'pos' : 'neg'">
{{ m.growth >= 0 ? '+' : '' }}{{ m.growth }}%
</text>
</view>
</view>
<!-- 寰幆鎾斁锛氬鍒朵竴浠芥暟鎹敤浜庢棤缂濇粴鍔?-->
<!-- 循环播放:复制一份数据用于无缝滚动 -->
<view v-if="topMerchants.length >= 6" v-for="m in topMerchants" :key="'copy-' + m.id" class="rank-item">
<text class="rank-no">{{ m.rank }}</text>
<text class="rank-name">{{ m.name }}</text>
<view class="rank-right">
<text class="rank-val">{{ formatMoney(m.sales) }}</text>
<text class="rank-val">¥{{ formatMoney(m.sales) }}</text>
<text class="chip" :class="m.growth >= 0 ? 'pos' : 'neg'">
{{ m.growth >= 0 ? '+' : '' }}{{ m.growth }}%
</text>
@@ -224,47 +225,47 @@
</view>
</view>
<!-- 蹇€熷伐鍏峰崱鐗囧尯锛?涓伐鍏峰叆鍙o級 -->
<!-- 快速工具卡片区6个工具入口 -->
<view class="tools-section">
<view class="section-header">
<text class="section-title">蹇€熷垎鏋愬伐鍏?/text>
<text class="section-desc">鐐瑰嚮杩涘叆璇︾粏鍒嗘瀽</text>
<text class="section-title">快速分析工具</text>
<text class="section-desc">点击进入详细分析</text>
</view>
<view class="tools-grid">
<view class="tool-card" @click="goToSalesReport">
<view class="tool-icon sales">馃搳</view>
<text class="tool-title">閿€鍞姤琛?/text>
<text class="tool-desc">GMV銆佽鍗曘€佽浆鍖栫巼</text>
<view class="tool-icon sales">📊</view>
<text class="tool-title">销售报表</text>
<text class="tool-desc">GMV、订单、转化率</text>
</view>
<view class="tool-card" @click="goToUserAnalysis">
<view class="tool-icon users">馃懃</view>
<text class="tool-title">鐢ㄦ埛鍒嗘瀽</text>
<text class="tool-desc">澧為暱銆佹椿璺冦€佺暀瀛?/text>
<view class="tool-icon users">👥</view>
<text class="tool-title">用户分析</text>
<text class="tool-desc">增长、活跃、留存</text>
</view>
<view class="tool-card" @click="goToProductInsights">
<view class="tool-icon products">馃摝</view>
<text class="tool-title">鍟嗗搧娲炲療</text>
<text class="tool-desc">閿€閲忋€佸簱瀛樸€佷环鏍?/text>
<view class="tool-icon products">📦</view>
<text class="tool-title">商品洞察</text>
<text class="tool-desc">销量、库存、价格</text>
</view>
<view class="tool-card" @click="goToMarketTrends">
<view class="tool-icon market">馃搱</view>
<text class="tool-title">甯傚満瓒嬪娍</text>
<text class="tool-desc">鏁翠綋瓒嬪娍銆佽涓氬姣?/text>
<view class="tool-icon market">📈</view>
<text class="tool-title">市场趋势</text>
<text class="tool-desc">整体趋势、行业对比</text>
</view>
<view class="tool-card" @click="goToCouponAnalysis">
<view class="tool-icon coupon">馃帿</view>
<text class="tool-title">浼樻儬鍒稿垎鏋?/text>
<text class="tool-desc">鍙戞斁銆佷娇鐢ㄣ€丷OI</text>
<view class="tool-icon coupon">🎫</view>
<text class="tool-title">优惠券分析</text>
<text class="tool-desc">发放、使用、ROI</text>
</view>
<view class="tool-card" @click="goToCustomReport">
<view class="tool-icon custom">鈿欙笍</view>
<text class="tool-title">鑷畾涔夋姤琛?/text>
<text class="tool-desc">鍒涘缓涓撳睘鎶ヨ〃</text>
<view class="tool-icon custom">⚙️</view>
<text class="tool-title">自定义报表</text>
<text class="tool-desc">创建专属报表</text>
</view>
</view>
</view>
<!-- 鐣欑櫧 -->
<!-- 留白 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -301,10 +302,10 @@ const autoRefreshInterval = ref(60000)
const autoRefreshTimer = ref<any>(null)
const timePeriods = ref<Array<{ value: string; label: string }>>([
{ value: '7d', label: '7澶? },
{ value: '30d', label: '30澶? },
{ value: '90d', label: '90澶? },
{ value: '1y', label: '1骞? }
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
{ value: '90d', label: '90天' },
{ value: '1y', label: '1年' }
])
const realTime = reactive({
@@ -328,7 +329,7 @@ const userSegmentOption = ref<any>({})
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7澶?
return p ? p.label : '7天'
})
function updateTime() {
@@ -363,7 +364,7 @@ async function loadTrend() {
trend.gmv = data.gmv
trend.orders = data.orders
} catch (e) {
console.error('鉂?loadTrend failed', e)
console.error('loadTrend failed', e)
trend.x = []
trend.gmv = []
trend.orders = []
@@ -381,7 +382,7 @@ async function loadRealTime() {
realTime.conversion_rate = data.conversion_rate
realTime.conversion_growth = data.conversion_growth
} catch (e) {
console.error('鉂?loadRealTime failed', e)
console.error('loadRealTime failed', e)
}
}
@@ -393,7 +394,7 @@ async function loadTopProducts() {
const list = await fetchDashboardTopProducts(selectedPeriod.value, 50, range)
topProducts.splice(0, topProducts.length, ...list)
} catch (e) {
console.error('鉂?loadTopProducts failed', e)
console.error('loadTopProducts failed', e)
topProducts.splice(0, topProducts.length)
}
}
@@ -406,7 +407,7 @@ async function loadTopMerchants() {
const list = await fetchDashboardTopMerchants(selectedPeriod.value, 50, range)
topMerchants.splice(0, topMerchants.length, ...list)
} catch (e) {
console.error('鉂?loadTopMerchants failed', e)
console.error('loadTopMerchants failed', e)
topMerchants.splice(0, topMerchants.length)
}
}
@@ -419,7 +420,7 @@ async function loadUserSegments() {
const list = await fetchDashboardUserSegments(selectedPeriod.value, range)
userSegments.splice(0, userSegments.length, ...list)
} catch (e) {
console.error('鉂?loadUserSegments failed', e)
console.error('loadUserSegments failed', e)
userSegments.splice(0, userSegments.length)
}
}
@@ -432,7 +433,7 @@ async function loadTrafficSources() {
const list = await fetchDashboardTrafficSources(selectedPeriod.value, range)
trafficSources.splice(0, trafficSources.length, ...list)
} catch (e) {
console.error('鉂?loadTrafficSources failed', e)
console.error('loadTrafficSources failed', e)
trafficSources.splice(0, trafficSources.length)
}
}
@@ -468,12 +469,12 @@ function toPlainObject(obj: any): any {
}
function buildChartOptions() {
console.log('馃搳 buildChartOptions: 寮€濮嬫瀯寤哄浘琛ㄩ厤缃?)
console.log('馃搳 buildChartOptions: trafficSources', trafficSources, '鏁伴噺:', trafficSources.length)
console.log('馃搳 buildChartOptions: userSegments', userSegments, '鏁伴噺:', userSegments.length)
console.log('📊 buildChartOptions: 开始构建图表配置')
console.log('📊 buildChartOptions: trafficSources', trafficSources, '数量:', trafficSources.length)
console.log('📊 buildChartOptions: userSegments', userSegments, '数量:', userSegments.length)
if (!trafficSources || !userSegments) {
console.warn('鈿狅笍 buildChartOptions: 鏁版嵁鏈噯澶囧ソ锛岃烦杩囨瀯寤?)
console.warn('⚠️ buildChartOptions: 数据未准备好,跳过构建')
return
}
@@ -484,15 +485,15 @@ function buildChartOptions() {
})
const total = trafficY.reduce((sum, v) => sum + v, 0)
console.log('馃搳 buildChartOptions: 娴侀噺鏉ユ簮鏁版嵁', { trafficX, trafficY, total, count: trafficX.length })
console.log('📊 buildChartOptions: 流量来源数据', { trafficX, trafficY, total, count: trafficX.length })
if (trafficX.length === 0 || total === 0) {
console.warn('鈿狅笍 buildChartOptions: 娴侀噺鏉ユ簮鏁版嵁涓虹┖锛屼娇鐢ㄥ崰浣嶆暟鎹?)
console.warn('⚠️ buildChartOptions: 流量来源数据为空,使用占位数据')
trafficBarOption.value = toPlainObject({
grid: { left: 80, right: 24, top: 18, bottom: 18 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' } },
yAxis: { type: 'category', data: ['鏆傛棤鏁版嵁'], axisTick: { show: false } },
yAxis: { type: 'category', data: ['暂无数据'], axisTick: { show: false } },
series: [{ type: 'bar', data: [0], barWidth: 14 }]
})
} else {
@@ -504,7 +505,7 @@ function buildChartOptions() {
formatter: (params: any) => {
const p = params[0]
const percent = total > 0 ? ((p.value / total) * 100).toFixed(1) : '0'
return `${p.name}<br/>${p.marker} ${p.value} 娆?(${percent}%)`
return `${p.name}<br/>${p.marker} ${p.value} (${percent}%)`
}
},
xAxis: {
@@ -546,7 +547,7 @@ function buildChartOptions() {
trafficBarOption.value = toPlainObject(newTrafficOption)
}
console.log('馃搳 buildChartOptions: trafficBarOption 鏋勫缓瀹屾垚', trafficBarOption.value)
console.log('📊 buildChartOptions: trafficBarOption 构建完成', trafficBarOption.value)
const segmentData = userSegments.map((it) => ({
name: String(it.name),
@@ -556,10 +557,10 @@ function buildChartOptions() {
})()
}))
console.log('馃搳 buildChartOptions: 鐢ㄦ埛缁撴瀯鏁版嵁', segmentData, '鏁伴噺:', segmentData.length)
console.log('📊 buildChartOptions: 用户结构数据', segmentData, '数量:', segmentData.length)
if (segmentData.length === 0) {
console.warn('鈿狅笍 buildChartOptions: 鐢ㄦ埛缁撴瀯鏁版嵁涓虹┖锛屼娇鐢ㄥ崰浣嶆暟鎹?)
console.warn('⚠️ buildChartOptions: 用户结构数据为空,使用占位数据')
userSegmentOption.value = toPlainObject({
tooltip: { trigger: 'item' },
legend: { left: 0, bottom: 0, itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 12 } },
@@ -569,7 +570,7 @@ function buildChartOptions() {
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '45%'],
data: [{ name: '鏆傛棤鏁版嵁', value: 1 }],
data: [{ name: '暂无数据', value: 1 }],
label: { show: true, formatter: '{b}\n{d}%' }
}
]
@@ -609,14 +610,14 @@ function buildChartOptions() {
userSegmentOption.value = toPlainObject(newUserSegmentOption)
}
console.log('馃搳 buildChartOptions: userSegmentOption 鏋勫缓瀹屾垚', userSegmentOption.value)
console.log('馃搳 buildChartOptions: 鍥捐〃閰嶇疆鏋勫缓瀹屾垚')
console.log('📊 buildChartOptions: userSegmentOption 构建完成', userSegmentOption.value)
console.log('📊 buildChartOptions: 图表配置构建完成')
}
watch(
trafficSources,
(newVal) => {
console.log('馃憖 watch trafficSources 瑙﹀彂', newVal)
console.log('👀 watch trafficSources 触发', newVal)
if (newVal && newVal.length > 0) {
buildChartOptions()
}
@@ -627,7 +628,7 @@ watch(
watch(
userSegments,
(newVal) => {
console.log('馃憖 watch userSegments 瑙﹀彂', newVal)
console.log('👀 watch userSegments 触发', newVal)
if (newVal && newVal.length > 0) {
buildChartOptions()
}
@@ -648,26 +649,26 @@ async function refreshAll() {
])
updateTime()
console.log('鉁?refreshAll: 鎵€鏈夋暟鎹姞杞藉畬鎴愶紝寮€濮嬫瀯寤哄浘琛?)
console.log('refreshAll: 所有数据加载完成,开始构建图表')
await new Promise((resolve) => {
if (typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(() => {
buildChartOptions()
console.log('鉁?refreshAll: 鍥捐〃閰嶇疆鏋勫缓瀹屾垚')
console.log('refreshAll: 图表配置构建完成')
resolve(null)
})
} else {
setTimeout(() => {
buildChartOptions()
console.log('鉁?refreshAll: 鍥捐〃閰嶇疆鏋勫缓瀹屾垚')
console.log('refreshAll: 图表配置构建完成')
resolve(null)
}, 50)
}
})
} catch (e) {
console.error('鉂?refreshAll failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '鏁版嵁鍔犺浇澶辫触' }), icon: 'none' })
console.error('refreshAll failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '数据加载失败' }), icon: 'none' })
}
}
@@ -686,7 +687,7 @@ async function initDashboard() {
async function selectPeriod(p: string) {
selectedPeriod.value = p
// 鍒囨崲鍒板揩鎹锋椂闂存鏃讹紝閫€鍑鸿嚜瀹氫箟鑼冨洿
// 切换到快捷时间段时,退出自定义范围
customRangeEnabled.value = false
selectedStartDate.value = ''
selectedEndDate.value = ''
@@ -699,19 +700,19 @@ async function selectPeriod(p: string) {
if (typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(() => {
buildChartOptions()
console.log('鉁?selectPeriod: 鍥捐〃閰嶇疆鏋勫缓瀹屾垚')
console.log('selectPeriod: 图表配置构建完成')
resolve(null)
})
} else {
setTimeout(() => {
buildChartOptions()
console.log('鉁?selectPeriod: 鍥捐〃閰嶇疆鏋勫缓瀹屾垚')
console.log('selectPeriod: 图表配置构建完成')
resolve(null)
}, 50)
}
})
} catch (e) {
console.error('鉂?selectPeriod failed', e)
console.error('selectPeriod failed', e)
} finally {
loading.value = false
}
@@ -730,30 +731,30 @@ function closeMoreMenu() {
}
function handleSearch() {
uni.showToast({ title: '鎼滅储鍔熻兘', icon: 'none' })
uni.showToast({ title: '搜索功能', icon: 'none' })
}
function handleNotification() {
uni.showToast({ title: '閫氱煡涓績', icon: 'none' })
uni.showToast({ title: '通知中心', icon: 'none' })
}
function handleFullscreen() {
uni.showToast({ title: '鍏ㄥ睆妯″紡', icon: 'none' })
uni.showToast({ title: '全屏模式', icon: 'none' })
}
function handleMobile() {
uni.showToast({ title: '绉诲姩绔瑙?, icon: 'none' })
uni.showToast({ title: '移动端预览', icon: 'none' })
}
function handleDropdown() {
uni.showActionSheet({
itemList: ['crmeb demo', '鍒囨崲椤圭洰', '椤圭洰璁剧疆'],
itemList: ['crmeb demo', '切换项目', '项目设置'],
success: () => {}
})
}
function handleSettings() {
uni.showToast({ title: '璁剧疆', icon: 'none' })
uni.showToast({ title: '设置', icon: 'none' })
}
function toggleCustomRange() {
@@ -776,13 +777,13 @@ function onDateRangeClear() {
function formatInt(n: number): string {
const v = isFinite(n) ? Math.round(n) : 0
if (v >= 10000) return (v / 10000).toFixed(1) + '涓?
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v.toString()
}
function formatMoney(n: number): string {
const v = isFinite(n) ? n : 0
if (v >= 10000) return (v / 10000).toFixed(1) + '涓?
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
return v.toFixed(0)
}
@@ -844,22 +845,22 @@ function toggleAutoRefresh() {
autoRefreshEnabled.value = !autoRefreshEnabled.value
if (autoRefreshEnabled.value) {
startAutoRefresh()
uni.showToast({ title: '宸插紑鍚嚜鍔ㄥ埛鏂?, icon: 'success' })
uni.showToast({ title: '已开启自动刷新', icon: 'success' })
} else {
stopAutoRefresh()
uni.showToast({ title: '宸插叧闂嚜鍔ㄥ埛鏂?, icon: 'none' })
uni.showToast({ title: '已关闭自动刷新', icon: 'none' })
}
}
function exportReport() {
uni.showActionSheet({
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
})
}
onLoad(() => {
if (!ensureAnalyticsLogin({ toastTitle: '璇峰厛鐧诲綍鍚庢煡鐪嬫暟鎹垎鏋? })) return
if (!ensureAnalyticsLogin({ toastTitle: '请先登录后查看数据分析' })) return
initDashboard()
})
@@ -880,15 +881,15 @@ onHide(() => {
</script>
<style>
/* 椤甸潰锛氱櫧搴?+ 瀹藉睆灞呬腑 + 鑷€傚簲 */
/* 璇存槑锛歶ni-app 鐨?rpx 浼氶殢灞忓缂╂斁锛屽灞?H5 寤鸿鐢?max-width 鎺у埗鍐呭瀹藉害銆?*/
/* 页面:白底 + 宽屏居中 + 自适应 */
/* 说明uni-app rpx 会随屏宽缩放,宽屏 H5 建议用 max-width 控制内容宽度。 */
.page {
min-height: 100vh;
background: #f6f7fb;
}
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -900,19 +901,19 @@ onHide(() => {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
/* padding removed */ 16px 28px;
padding: 16px 16px 28px;
box-sizing: border-box;
flex: 1;
}
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
/* 响应式:窄屏时全屏显示 */
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -923,8 +924,8 @@ onHide(() => {
}
}
/* 椤堕儴 */
/* 鉁?寮哄埗锛氶《閮ㄥ繀椤绘í鎺掞紙閬垮厤琚叏灞€ view:flex-direction:column 褰卞搷锛?*/
/* 顶部 */
/* ✅ 强制:顶部必须横排(避免被全局 view:flex-direction:column 影响) */
.topbar {
display: flex;
flex-direction: row !important;
@@ -972,13 +973,13 @@ onHide(() => {
line-height: 1;
}
/* 宸︿晶鏍囬缁勪粛鐒舵槸绾靛悜 */
/* 左侧标题组仍然是纵向 */
.title-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
min-width: 0; /* 鍏佽鍐呴儴 text 鍋氱渷鐣?*/
min-width: 0; /* 允许内部 text 做省略 */
}
.title {
@@ -1000,7 +1001,7 @@ onHide(() => {
white-space: nowrap;
}
/* 鉁?鍙充晶鎸夐挳姘镐笉鎹㈡垚绔栧垪锛堝繀瑕佹椂鍙鍒囷紝涓嶆崲琛岋級 */
/* ✅ 右侧按钮永不换成竖列(必要时只裁切,不换行) */
.topbar-right {
display: flex;
flex-direction: row !important;
@@ -1025,7 +1026,7 @@ onHide(() => {
color: #fff;
}
/* 鍥炬爣鎸夐挳鏍峰紡 */
/* 图标按钮样式 */
.icon-btn-icon {
width: 32px;
height: 32px;
@@ -1049,7 +1050,7 @@ onHide(() => {
line-height: 1;
}
/* 閫氱煡鍥炬爣甯︾孩鐐?*/
/* 通知图标带红点 */
.icon-btn-icon.notification .badge {
position: absolute;
top: 4px;
@@ -1061,7 +1062,7 @@ onHide(() => {
border: 1px solid #fff;
}
/* 涓嬫媺鑿滃崟 */
/* 下拉菜单 */
.dropdown {
display: flex;
flex-direction: row !important;
@@ -1076,7 +1077,7 @@ onHide(() => {
flex-shrink: 0;
}
/* 鏇村鎸夐挳锛堥粯璁ら殣钘忥紝绐勫睆鏃舵樉绀猴級 */
/* 更多按钮(默认隐藏,窄屏时显示) */
.more-btn {
display: none;
width: 32px;
@@ -1101,7 +1102,7 @@ onHide(() => {
color: #111;
}
/* 鏇村鑿滃崟涓嬫媺 */
/* 更多菜单下拉 */
.more-menu {
position: absolute;
top: calc(100% + 8px);
@@ -1156,8 +1157,8 @@ onHide(() => {
line-height: 1;
}
/* KPI锛氶粯璁?2鍒楋紝瀹藉睆 4鍒?*/
/* 鉁?鏍稿績淇锛氱敤 flex + calc(50%) 鏇夸唬 width锛岄伩鍏?rpx + CSS var 澶辨晥 */
/* KPI:默认 2列宽屏 4列 */
/* ✅ 核心修复:用 flex + calc(50%) 替代 width,避免 rpx + CSS var 失效 */
.kpi-grid {
margin-top: 12px;
display: flex;
@@ -1186,7 +1187,7 @@ onHide(() => {
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
/* KPI 鍗$墖娓愬彉鑳屾櫙 */
/* KPI 卡片渐变背景 */
.kpi-card-gmv {
background: linear-gradient(135deg, #FF6B6B 0%, #FF4D4F 100%);
color: #fff;
@@ -1268,7 +1269,7 @@ onHide(() => {
color: #fff;
}
/* 鏃堕棿缁村害 tabs 妯帓 */
/* 时间维度 tabs 横排 */
.tabs {
margin-top: 12px;
display: flex;
@@ -1298,7 +1299,7 @@ onHide(() => {
color: #fff;
}
/* 鍗$墖 */
/* 卡片 */
.card {
margin-top: 12px;
background: #fff;
@@ -1335,17 +1336,17 @@ onHide(() => {
color: rgba(0,0,0,0.55);
}
/* 鍥捐〃蹇呴』缁欓珮搴︼紙H5 鍚﹀垯鍙兘 0 楂橈級 */
/* 图表必须给高度H5 否则可能 0 高) */
.chart-box {
width: 100%;
height: 360px; /* 寤鸿鐢?px锛孒5 鏇寸ǔ */
height: 360px; /* 建议用 pxH5 更稳 */
}
.fullwide .chart-box {
height: 420px; /* 澶у浘鏇撮珮 */
height: 420px; /* 大图更高 */
}
/* 鍥捐〃鍔犺浇鐘舵€?*/
/* 图表加载状态 */
.chart-loading {
width: 100%;
height: 360px;
@@ -1360,7 +1361,7 @@ onHide(() => {
height: 420px;
}
/* 鐢ㄦ埛缁撴瀯鍜屾祦閲忔潵婧愶細妯帓鏄剧ず */
/* 用户结构和流量来源:横排显示 */
.charts-row {
display: flex;
flex-direction: row !important;
@@ -1376,7 +1377,7 @@ onHide(() => {
min-width: 360px;
}
/* 涓や釜TOP鎺掕锛氭í鎺掓樉绀?*/
/* 两个TOP排行横排显示 */
.tops-row {
display: flex;
flex-direction: row !important;
@@ -1392,7 +1393,7 @@ onHide(() => {
min-width: 360px;
}
/* 婊氬姩瀹瑰櫒 */
/* 滚动容器 */
.rank-scroll-container {
width: 100%;
height: 300px;
@@ -1408,7 +1409,7 @@ onHide(() => {
animation: scrollRank 15s linear infinite;
}
/* 鍒楄〃鏍峰紡 */
/* 列表样式 */
.rank-list {
display: flex;
flex-direction: column;
@@ -1416,14 +1417,14 @@ onHide(() => {
width: 100%;
}
/* 婊氬姩鍔ㄧ敾锛氬綋鏁版嵁瓒呰繃5鏉℃椂鑷姩婊氬姩 */
/* 滚动动画当数据超过5条时自动滚动 */
@keyframes scrollRank {
0% {
transform: translateY(0);
}
100% {
/* 婊氬姩鍒扮涓€浠芥暟鎹殑鏈熬锛屽疄鐜版棤缂濆惊鐜?*/
/* 姣忔潯 rank-item 楂樺害绾?50px锛堝寘鎷?padding 鍜?gap锛夛紝婊氬姩涓€鍗婇珮搴?*/
/* 滚动到第一份数据的末尾,实现无缝循环 */
/* 每条 rank-item 高度约 50px包括 padding gap),滚动一半高度 */
transform: translateY(calc(-50%));
}
}
@@ -1486,14 +1487,14 @@ onHide(() => {
color: #dc2626;
}
/* 瀹藉睆锛欿PI 4鍒?*/
/* 宽屏KPI 4*/
@media screen and (min-width: 960px) {
.kpi-card {
flex: 1 1 calc(25% - 9px);
min-width: 200px;
}
/* 瀹藉睆鏃舵樉绀烘墍鏈夋寜閽紝闅愯棌"鏇村"鎸夐挳 */
/* 宽屏时显示所有按钮,隐藏"更多"按钮 */
.topbar-right .btn-hidden {
display: flex !important;
}
@@ -1502,13 +1503,13 @@ onHide(() => {
display: none !important;
}
/* 瀹藉睆鏃跺伐鍏峰崱鐗?3鍒?*/
/* 宽屏时工具卡片 3列 */
.tools-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* 鑷€傚簲锛氱獎灞忚嚜鍔ㄥ彉涓€鍒楋紙鏂偣鐢?px锛?*/
/* 自适应:窄屏自动变一列(断点用 px */
@media screen and (max-width: 960px) {
.charts-row,
.tops-row {
@@ -1528,22 +1529,22 @@ onHide(() => {
height: 360px;
}
/* 椤堕儴鏍忔寜閽湪灏忓睆骞曚笂锛氶殣钘忛儴鍒嗘寜閽紝鏄剧ず"鏇村"鎸夐挳 */
/* 顶部栏按钮在小屏幕上:隐藏部分按钮,显示"更多"按钮 */
.topbar-right {
gap: 6px;
}
/* 闅愯棌鏍囪涓?btn-hidden 鐨勬寜閽?*/
/* 隐藏标记为 btn-hidden 的按钮 */
.topbar-right .btn-hidden {
display: none !important;
}
/* 鏄剧ず"鏇村"鎸夐挳 */
/* 显示"更多"按钮 */
.more-btn {
display: flex !important;
}
/* 鏍囬鍦ㄧ獎灞忔椂鍏佽鐪佺暐鍙?*/
/* 标题在窄屏时允许省略号 */
.title,
.subtitle {
max-width: 200px;
@@ -1568,18 +1569,18 @@ onHide(() => {
font-size: 12px;
}
/* 绐勫睆鏃?KPI 鍗$墖鍗曞垪 */
/* 窄屏时 KPI 卡片单列 */
.kpi-card {
flex: 1 1 100%;
}
/* 绐勫睆鏃跺伐鍏峰崱鐗?2鍒?*/
/* 窄屏时工具卡片 2列 */
.tools-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 蹇€熷伐鍏峰崱鐗囧尯 */
/* 快速工具卡片区 */
.tools-section {
margin-top: 24px;
}
@@ -1679,4 +1680,3 @@ onHide(() => {
}
</style>