优化细节

This commit is contained in:
2026-02-06 12:06:33 +08:00
parent b7545173c6
commit d00f0b7412
83 changed files with 3901 additions and 2354 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,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,67 +54,67 @@
@clear="onDateRangeClear"
/>
<!-- KPI 指标卡片 -->
<!-- KPI 鎸囨爣鍗$墖 -->
<view class="kpi-grid">
<view class="kpi-card">
<text class="kpi-label">发放总数</text>
<text class="kpi-label">鍙戞斁鎬绘暟</text>
<text class="kpi-value">{{ formatInt(couponData.total_issued) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(couponData.issued_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(couponData.issued_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">使用数量</text>
<text class="kpi-label">浣跨敤鏁伴噺</text>
<text class="kpi-value">{{ formatInt(couponData.total_used) }}</text>
<text class="kpi-meta">使用率:{{ formatPct(couponData.usage_rate) }}</text>
<text class="kpi-meta">浣跨敤鐜囷細{{ formatPct(couponData.usage_rate) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">GMV 提升</text>
<text class="kpi-value">¥{{ formatMoney(couponData.gmv_increase) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(couponData.gmv_growth) }}</text>
<text class="kpi-label">GMV 鎻愬崌</text>
<text class="kpi-value">{{ formatMoney(couponData.gmv_increase) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(couponData.gmv_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">ROI</text>
<text class="kpi-value">{{ formatPct(couponData.roi) }}</text>
<text class="kpi-meta">投入产出比</text>
<text class="kpi-meta">鎶曞叆浜у嚭姣?/text>
</view>
</view>
<!-- 优惠券类型分析 -->
<!-- 浼樻儬鍒哥被鍨嬪垎鏋?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">优惠券类型分析</text>
<text class="card-desc">8种券类型:满减券、折扣券、免运费券、新人券、会员券、品类券、商家券、限时券</text>
<text class="card-title">浼樻儬鍒哥被鍨嬪垎鏋?/text>
<text class="card-desc">8绉嶅埜绫诲瀷锛氭弧鍑忓埜銆佹姌鎵e埜銆佸厤杩愯垂鍒搞€佹柊浜哄埜銆佷細鍛樺埜銆佸搧绫诲埜銆佸晢瀹跺埜銆侀檺鏃跺埜</text>
</view>
<EChartsView class="chart-box" :option="typeChartOption" />
</view>
<!-- 发放渠道效果 -->
<!-- 鍙戞斁娓犻亾鏁堟灉 -->
<view class="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>
<EChartsView class="chart-box" :option="channelChartOption" />
</view>
<!-- 优惠券使用趋势 -->
<!-- 浼樻儬鍒镐娇鐢ㄨ秼鍔?-->
<view class="card">
<view class="card-head">
<text class="card-title">优惠券使用趋势</text>
<text class="card-desc">{{ selectedPeriodText }} · 发放 vs 使用</text>
<text class="card-title">浼樻儬鍒镐娇鐢ㄨ秼鍔?/text>
<text class="card-desc">{{ selectedPeriodText }} 路 鍙戞斁 vs 浣跨敤</text>
</view>
<EChartsView class="chart-box" :option="trendChartOption" />
</view>
<!-- 优惠券转化效果 -->
<!-- 浼樻儬鍒歌浆鍖栨晥鏋?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">优惠券转化效果</text>
<text class="card-desc">GMV提升、订单增长</text>
<text class="card-title">浼樻儬鍒歌浆鍖栨晥鏋?/text>
<text class="card-desc">GMV鎻愬崌銆佽鍗曞闀?/text>
</view>
<EChartsView class="chart-box" :option="conversionChartOption" />
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -147,10 +146,10 @@ const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/coupon-analysis')
const timePeriods = ref<Array<TimePeriod>>([
{ 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 couponData = ref<CouponData>({
@@ -168,7 +167,7 @@ const channelChartOption = ref({} as any)
const trendChartOption = ref({} as any)
const conversionChartOption = ref({} as any)
// 原始数据
// 鍘熷鏁版嵁
const _typeRows = ref<Array<UTSJSONObject>>([])
const _channelRows = ref<Array<UTSJSONObject>>([])
const _trendRows = ref<Array<UTSJSONObject>>([])
@@ -176,7 +175,7 @@ const _conversionRows = ref<Array<UTSJSONObject>>([])
const selectedPeriodText = computed(() => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
onLoad(() => {
@@ -246,7 +245,7 @@ async function loadCouponData() {
console.error('loadCouponData failed:', e)
updateTime()
buildChartOptions()
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '优惠券分析数据加载失败' }), icon: 'none' })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '浼樻儬鍒稿垎鏋愭暟鎹姞杞藉け璐? }), icon: 'none' })
}
}
@@ -278,13 +277,13 @@ function onDateRangeClear() {
function refreshData() {
loadCouponData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -297,13 +296,13 @@ function updateTime() {
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)
}
@@ -319,8 +318,7 @@ function buildChartOptions() {
const trendRows = _trendRows.value
const convRows = _conversionRows.value
// 1) 券类型分析
const typeNames: string[] = []
// 1) 鍒哥被鍨嬪垎鏋? const typeNames: string[] = []
const typeIssued: number[] = []
const typeUsed: number[] = []
const typeUsageRate: number[] = []
@@ -328,15 +326,15 @@ function buildChartOptions() {
for (let i = 0; i < typeRows.length; i++) {
const r = typeRows[i]
const t = r.getNumber('coupon_type') ?? 0
let label = '未知'
if (t === 1) label = '满减券'
else if (t === 2) label = '折扣券'
else if (t === 3) label = '免运费券'
else if (t === 4) label = '新人券'
else if (t === 5) label = '会员券'
else if (t === 6) label = '品类券'
else if (t === 7) label = '商家券'
else if (t === 8) label = '限时券'
let label = '鏈煡'
if (t === 1) label = '婊″噺鍒?
else if (t === 2) label = '鎶樻墸鍒?
else if (t === 3) label = '鍏嶈繍璐瑰埜'
else if (t === 4) label = '鏂颁汉鍒?
else if (t === 5) label = '浼氬憳鍒?
else if (t === 6) label = '鍝佺被鍒?
else if (t === 7) label = '鍟嗗鍒?
else if (t === 8) label = '闄愭椂鍒?
typeNames.push(label)
typeIssued.push(r.getNumber('total_issued') ?? 0)
typeUsed.push(r.getNumber('total_used') ?? 0)
@@ -346,7 +344,7 @@ function buildChartOptions() {
typeChartOption.value = {
tooltip: { trigger: 'axis' },
legend: {
data: ['发放数量', '使用数量', '使用率'],
data: ['鍙戞斁鏁伴噺', '浣跨敤鏁伴噺', '浣跨敤鐜?],
top: 'bottom'
},
grid: { left: 40, right: 40, top: 40, bottom: 60 },
@@ -356,26 +354,26 @@ function buildChartOptions() {
axisLabel: { interval: 0, rotate: 20 }
},
yAxis: [
{ type: 'value', name: '数量' },
{ type: 'value', name: '使用率', min: 0, max: 100, position: 'right' }
{ type: 'value', name: '鏁伴噺' },
{ type: 'value', name: '浣跨敤鐜?, min: 0, max: 100, position: 'right' }
],
series: [
{
name: '发放数量',
name: '鍙戞斁鏁伴噺',
type: 'bar',
data: typeIssued,
barMaxWidth: 22,
itemStyle: { color: '#3b82f6' }
},
{
name: '使用数量',
name: '浣跨敤鏁伴噺',
type: 'bar',
data: typeUsed,
barMaxWidth: 22,
itemStyle: { color: '#22c55e' }
},
{
name: '使用率',
name: '浣跨敤鐜?,
type: 'line',
yAxisIndex: 1,
smooth: true,
@@ -389,7 +387,7 @@ function buildChartOptions() {
]
}
// 2) 发放渠道效果
// 2) 鍙戞斁娓犻亾鏁堟灉
const channelNames: string[] = []
const channelIssued: number[] = []
const channelUsed: number[] = []
@@ -398,13 +396,13 @@ function buildChartOptions() {
const r = channelRows[i]
const ch = r.getString('channel') ?? ''
let chLabel = ch
if (ch === 'manual') chLabel = '主动领取'
else if (ch === 'auto') chLabel = '自动发放'
else if (ch === 'campaign') chLabel = '活动赠送'
else if (ch === 'invite') chLabel = '邀请奖励'
else if (ch === 'cs') chLabel = '客服赠送'
else if (ch === 'points') chLabel = '积分兑换'
else if (ch.trim() === '') chLabel = '未知'
if (ch === 'manual') chLabel = '涓诲姩棰嗗彇'
else if (ch === 'auto') chLabel = '鑷姩鍙戞斁'
else if (ch === 'campaign') chLabel = '娲诲姩璧犻€?
else if (ch === 'invite') chLabel = '閭€璇峰鍔?
else if (ch === 'cs') chLabel = '瀹㈡湇璧犻€?
else if (ch === 'points') chLabel = '绉垎鍏戞崲'
else if (ch.trim() === '') chLabel = '鏈煡'
channelNames.push(chLabel)
channelIssued.push(r.getNumber('total_issued') ?? 0)
channelUsed.push(r.getNumber('total_used') ?? 0)
@@ -412,17 +410,17 @@ function buildChartOptions() {
channelChartOption.value = {
tooltip: { trigger: 'axis' },
legend: { data: ['发放数量', '使用数量'], top: 'bottom' },
legend: { data: ['鍙戞斁鏁伴噺', '浣跨敤鏁伴噺'], top: 'bottom' },
grid: { left: 80, right: 30, top: 20, bottom: 60 },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: channelNames },
series: [
{ name: '发放数量', type: 'bar', data: channelIssued },
{ name: '使用数量', type: 'bar', data: channelUsed }
{ name: '鍙戞斁鏁伴噺', type: 'bar', data: channelIssued },
{ name: '浣跨敤鏁伴噺', type: 'bar', data: channelUsed }
]
}
// 3) 使用趋势
// 3) 浣跨敤瓒嬪娍
const trendDays: string[] = []
const trendIssued: number[] = []
const trendUsed: number[] = []
@@ -437,17 +435,17 @@ function buildChartOptions() {
trendChartOption.value = {
tooltip: { trigger: 'axis' },
legend: { data: ['发放数量', '使用数量'], top: 'bottom' },
legend: { data: ['鍙戞斁鏁伴噺', '浣跨敤鏁伴噺'], top: 'bottom' },
grid: { left: 40, right: 20, top: 40, bottom: 60 },
xAxis: { type: 'category', data: trendDays },
yAxis: { type: 'value', name: '数量' },
yAxis: { type: 'value', name: '鏁伴噺' },
series: [
{ name: '发放数量', type: 'bar', data: trendIssued },
{ name: '使用数量', type: 'line', smooth: true, data: trendUsed }
{ name: '鍙戞斁鏁伴噺', type: 'bar', data: trendIssued },
{ name: '浣跨敤鏁伴噺', type: 'line', smooth: true, data: trendUsed }
]
}
// 4) 转化效果
// 4) 杞寲鏁堟灉
const convNames: string[] = []
const convWith: number[] = []
const convWithout: number[] = []
@@ -456,10 +454,10 @@ function buildChartOptions() {
const r = convRows[i]
const metric = r.getString('metric') ?? ''
let metricLabel = metric
if (metric === 'GMV') metricLabel = 'GMV(成交额)'
else if (metric === 'orders') metricLabel = '订单数'
else if (metric === 'avg_order_amount') metricLabel = '客单价'
else if (metric.trim() === '') metricLabel = '未知'
if (metric === 'GMV') metricLabel = 'GMV锛堟垚浜ら锛?
else if (metric === 'orders') metricLabel = '璁㈠崟鏁?
else if (metric === 'avg_order_amount') metricLabel = '瀹㈠崟浠?
else if (metric.trim() === '') metricLabel = '鏈煡'
convNames.push(metricLabel)
convWith.push(r.getNumber('with_coupon') ?? 0)
convWithout.push(r.getNumber('without_coupon') ?? 0)
@@ -467,13 +465,13 @@ function buildChartOptions() {
conversionChartOption.value = {
tooltip: { trigger: 'axis' },
legend: { data: ['使用优惠券', '未使用优惠券'], top: 'bottom' },
legend: { data: ['浣跨敤浼樻儬鍒?, '鏈娇鐢ㄤ紭鎯犲埜'], top: 'bottom' },
grid: { left: 40, right: 20, top: 20, bottom: 60 },
xAxis: { type: 'category', data: convNames },
yAxis: { type: 'value' },
series: [
{ name: '使用优惠券', type: 'bar', data: convWith },
{ name: '未使用优惠券', type: 'bar', data: convWithout }
{ name: '浣跨敤浼樻儬鍒?, type: 'bar', data: convWith },
{ name: '鏈娇鐢ㄤ紭鎯犲埜', type: 'bar', data: convWithout }
]
}
}
@@ -494,13 +492,13 @@ function handleMenu() {
showSidebarMenu.value = true
}
// 模拟的 TopBar 事件处理
function handleSearch() { uni.showToast({ title: '搜索', icon: 'none' }) }
function handleNotification() { uni.showToast({ title: '通知', icon: 'none' }) }
function handleFullscreen() { uni.showToast({ title: '全屏', icon: 'none' }) }
function handleMobile() { uni.showToast({ title: '移动端', icon: 'none' }) }
function handleDropdown() { uni.showToast({ title: '下拉菜单', icon: 'none' }) }
function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
// 妯℃嫙鐨?TopBar 浜嬩欢澶勭悊
function handleSearch() { uni.showToast({ title: '鎼滅储', icon: 'none' }) }
function handleNotification() { uni.showToast({ title: '閫氱煡', icon: 'none' }) }
function handleFullscreen() { uni.showToast({ title: '鍏ㄥ睆', icon: 'none' }) }
function handleMobile() { uni.showToast({ title: '绉诲姩绔?, icon: 'none' }) }
function handleDropdown() { uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' }) }
function handleSettings() { uni.showToast({ title: '璁剧疆', icon: 'none' }) }
</script>
@@ -510,7 +508,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -522,18 +520,18 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -666,7 +664,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
color: #111;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
margin-top: 12px;
display: flex;
@@ -696,7 +694,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
color: #fff;
}
/* KPI 网格 */
/* KPI 缃戞牸 */
.kpi-grid {
margin-top: 12px;
display: flex;
@@ -733,7 +731,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
color: rgba(0,0,0,0.55);
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -772,7 +770,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
height: 360px;
}
/* 响应式 */
/* 鍝嶅簲寮?*/
@media screen and (min-width: 960px) {
.kpi-card {
flex: 1 1 calc(25% - 9px);
@@ -780,7 +778,7 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
}
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -806,3 +804,4 @@ function handleSettings() { uni.showToast({ title: '设置', icon: 'none' }) }
}
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<template>
<view class="page" @click="closeMoreMenu">
<!-- 固定顶部导航栏 -->
<!-- 鍥哄畾椤堕儴瀵艰埅鏍?-->
<AnalyticsTopBar
:title="'自定义报表'"
:lastUpdateTime="'创建和管理您的专属报表'"
:title="'鑷畾涔夋姤琛?"
:lastUpdateTime="'鍒涘缓鍜岀鐞嗘偍鐨勪笓灞炴姤琛?"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@refresh="refreshData"
@@ -16,90 +16,90 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 顶部操作区:新建报表 -->
<!-- 椤堕儴鎿嶄綔鍖猴細鏂板缓鎶ヨ〃 -->
<view class="toolbar">
<view class="toolbar-left">
<text class="toolbar-title">我的自定义报表</text>
<text class="toolbar-subtitle">按需组合指标和时间范围,生成专属报表</text>
<text class="toolbar-title">鎴戠殑鑷畾涔夋姤琛?/text>
<text class="toolbar-subtitle">鎸夐渶缁勫悎鎸囨爣鍜屾椂闂磋寖鍥达紝鐢熸垚涓撳睘鎶ヨ〃</text>
</view>
<view class="toolbar-right">
<button class="btn-primary" @click.stop="createReport"> 新建报表</button>
<button class="btn-primary" @click.stop="createReport">锛?鏂板缓鎶ヨ〃</button>
</view>
</view>
<!-- 报表列表 / 空状态 -->
<!-- 鎶ヨ〃鍒楄〃 / 绌虹姸鎬?-->
<view v-if="reports.length > 0" class="report-list">
<view v-for="report in reports" :key="report.id" class="report-card" @click="openReport(report)">
<view class="report-header">
<text class="report-title">{{ report.name }}</text>
<view class="report-actions">
<view class="action-btn" @click.stop="editReport(report)">
<text class="icon">✏️</text>
<text class="icon">鉁忥笍</text>
</view>
<view class="action-btn" @click.stop="deleteReport(report)">
<text class="icon">🗑️</text>
<text class="icon">馃棏锔?/text>
</view>
</view>
</view>
<text class="report-desc">{{ report.description || '点击进入报表详情查看数据' }}</text>
<text class="report-desc">{{ report.description || '鐐瑰嚮杩涘叆鎶ヨ〃璇︽儏鏌ョ湅鏁版嵁' }}</text>
<view class="report-meta">
<text class="meta-item">图表周期:{{ report.period || '自定义' }}</text>
<text class="meta-item">最近更新:{{ report.updated_at || '-' }}</text>
<text class="meta-item">鍥捐〃鍛ㄦ湡锛歿{ report.period || '鑷畾涔? }}</text>
<text class="meta-item">鏈€杩戞洿鏂帮細{{ report.updated_at || '-' }}</text>
</view>
</view>
</view>
<view v-else class="empty-state">
<text v-if="isLoggedIn" class="empty-title">暂无自定义报表</text>
<text v-else class="empty-title">请先登录</text>
<text v-if="isLoggedIn" class="empty-desc">点击下方按钮创建第一份报表,用于复用常看的指标组合。</text>
<text v-else class="empty-desc">创建自定义报表需要登录账号,请先登录后再使用此功能。</text>
<button v-if="isLoggedIn" class="btn-primary" @click.stop="createReport"> 新建报表</button>
<button v-else class="btn-primary" @click.stop="goToLogin">前往登录</button>
<text v-if="isLoggedIn" class="empty-title">鏆傛棤鑷畾涔夋姤琛?/text>
<text v-else class="empty-title">璇峰厛鐧诲綍</text>
<text v-if="isLoggedIn" class="empty-desc">鐐瑰嚮涓嬫柟鎸夐挳鍒涘缓绗竴浠芥姤琛紝鐢ㄤ簬澶嶇敤甯哥湅鐨勬寚鏍囩粍鍚堛€?/text>
<text v-else class="empty-desc">鍒涘缓鑷畾涔夋姤琛ㄩ渶瑕佺櫥褰曡处鍙凤紝璇峰厛鐧诲綍鍚庡啀浣跨敤姝ゅ姛鑳姐€?/text>
<button v-if="isLoggedIn" class="btn-primary" @click.stop="createReport">锛?鏂板缓鎶ヨ〃</button>
<button v-else class="btn-primary" @click.stop="goToLogin">鍓嶅線鐧诲綍</button>
</view>
<!-- 新建报表对话框 -->
<!-- 鏂板缓鎶ヨ〃瀵硅瘽妗?-->
<view class="modal" v-if="showCreateModal" @click.stop>
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ editingReport ? '编辑报表' : '新建报表' }}</text>
<text class="modal-title">{{ editingReport ? '缂栬緫鎶ヨ〃' : '鏂板缓鎶ヨ〃' }}</text>
<view class="modal-close" @click="closeModal">
<text class="icon">✕</text>
<text class="icon">鉁?/text>
</view>
</view>
<view class="modal-body">
<view class="form-item">
<text class="form-label">报表名称</text>
<text class="form-label">鎶ヨ〃鍚嶇О</text>
<input
class="form-input"
v-model="reportForm.name"
placeholder="请输入报表名称1-50个字符"
placeholder="璇疯緭鍏ユ姤琛ㄥ悕绉帮紙1-50涓瓧绗︼級"
@input="onNameInput"
/>
<text v-if="formErrors.name" class="form-error">{{ formErrors.name }}</text>
</view>
<view class="form-item">
<text class="form-label">报表描述</text>
<text class="form-label">鎶ヨ〃鎻忚堪</text>
<textarea
class="form-textarea"
v-model="reportForm.description"
placeholder="选填最多200个字符"
placeholder="閫夊~锛屾渶澶?00涓瓧绗?
@input="onDescriptionInput"
></textarea>
<text v-if="formErrors.description" class="form-error">{{ formErrors.description }}</text>
</view>
<view class="form-item">
<text class="form-label">选择指标</text>
<text class="form-label">閫夋嫨鎸囨爣</text>
<view class="metric-list">
<view
v-for="m in availableMetrics"
@@ -114,7 +114,7 @@
<text v-if="formErrors.metrics" class="form-error">{{ formErrors.metrics }}</text>
</view>
<view class="form-item">
<text class="form-label">时间维度</text>
<text class="form-label">鏃堕棿缁村害</text>
<view class="period-list">
<view
v-for="p in timePeriods"
@@ -129,7 +129,7 @@
<text v-if="formErrors.period" class="form-error">{{ formErrors.period }}</text>
</view>
<view class="form-item">
<text class="form-label">图表类型</text>
<text class="form-label">鍥捐〃绫诲瀷</text>
<view class="chart-type-list">
<view
v-for="t in chartTypes"
@@ -145,13 +145,13 @@
</view>
</view>
<view class="modal-footer">
<view class="btn btn-cancel" @click="closeModal">取消</view>
<view class="btn btn-primary" @click="saveReport">保存</view>
<view class="btn btn-cancel" @click="closeModal">鍙栨秷</view>
<view class="btn btn-primary" @click="saveReport">淇濆瓨</view>
</view>
</view>
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -202,31 +202,31 @@ const formErrors = reactive<ReportFormErrors>({
const availableMetrics = ref<Array<Metric>>([
{ key: 'gmv', label: 'GMV' },
{ key: 'orders', label: '订单数' },
{ key: 'users', label: '用户数' },
{ key: 'conversion', label: '转化率' },
{ key: 'avg_order', label: '客单价' },
{ key: 'repurchase', label: '复购率' }
{ key: 'orders', label: '璁㈠崟鏁? },
{ key: 'users', label: '鐢ㄦ埛鏁? },
{ key: 'conversion', label: '杞寲鐜? },
{ key: 'avg_order', label: '瀹㈠崟浠? },
{ key: 'repurchase', label: '澶嶈喘鐜? }
])
const timePeriods = ref<Array<TimePeriod>>([
{ 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 chartTypes = ref<Array<ChartType>>([
{ value: 'line', label: '折线图' },
{ value: 'bar', label: '柱状图' },
{ value: 'pie', label: '饼图' },
{ value: 'area', label: '面积图' },
{ value: 'combo', label: '组合图' }
{ value: 'line', label: '鎶樼嚎鍥? },
{ value: 'bar', label: '鏌辩姸鍥? },
{ value: 'pie', label: '楗煎浘' },
{ value: 'area', label: '闈㈢Н鍥? },
{ value: 'combo', label: '缁勫悎鍥? }
])
onLoad(() => {
currentPath.value = '/pages/mall/analytics/custom-report'
if (!ensureAnalyticsLogin({ toastTitle: '请先登录后使用自定义报表' })) return
if (!ensureAnalyticsLogin({ toastTitle: '璇峰厛鐧诲綍鍚庝娇鐢ㄨ嚜瀹氫箟鎶ヨ〃' })) return
loadReports()
})
@@ -264,7 +264,7 @@ async function loadReports() {
reports.splice(0, reports.length, ...list)
} catch (e) {
console.error('loadReports failed', e)
uni.showToast({ title: '报表加载失败', icon: 'none' })
uni.showToast({ title: '鎶ヨ〃鍔犺浇澶辫触', icon: 'none' })
}
}
@@ -306,8 +306,8 @@ function editReport(report: CustomReport) {
function deleteReport(report: CustomReport) {
uni.showModal({
title: '确认删除',
content: `确定要删除报表"${report.name}"吗?`,
title: '纭鍒犻櫎',
content: `纭畾瑕佸垹闄ゆ姤琛?${report.name}"鍚楋紵`,
success: (res) => {
if (res.confirm) {
doDeleteReport(report)
@@ -321,11 +321,11 @@ async function doDeleteReport(report: CustomReport) {
await ensureSupabaseReady()
await deleteCustomReport(report.id)
uni.showToast({ title: '删除成功', icon: 'success' })
uni.showToast({ title: '鍒犻櫎鎴愬姛', icon: 'success' })
loadReports()
} catch (e: any) {
console.error('doDeleteReport failed', e)
const errorMsg = e?.message || '删除失败'
const errorMsg = e?.message || '鍒犻櫎澶辫触'
uni.showToast({ title: errorMsg, icon: 'none' })
}
}
@@ -345,9 +345,9 @@ function toggleMetric(key: string) {
function onNameInput() {
const name = reportForm.name.trim()
if (name.length === 0) {
formErrors.name = '报表名称不能为空'
formErrors.name = '鎶ヨ〃鍚嶇О涓嶈兘涓虹┖'
} else if (name.length > 50) {
formErrors.name = '报表名称不能超过50个字符'
formErrors.name = '鎶ヨ〃鍚嶇О涓嶈兘瓒呰繃50涓瓧绗?
} else {
formErrors.name = ''
}
@@ -356,7 +356,7 @@ function onNameInput() {
function onDescriptionInput() {
const desc = reportForm.description
if (desc.length > 200) {
formErrors.description = '报表描述不能超过200个字符'
formErrors.description = '鎶ヨ〃鎻忚堪涓嶈兘瓒呰繃200涓瓧绗?
} else {
formErrors.description = ''
}
@@ -377,20 +377,20 @@ function validateReportForm(): boolean {
onDescriptionInput()
if (reportForm.metrics.length === 0) {
formErrors.metrics = '请至少选择一个指标'
formErrors.metrics = '璇疯嚦灏戦€夋嫨涓€涓寚鏍?
} else {
formErrors.metrics = ''
}
if (!reportForm.period) {
formErrors.period = '请选择时间维度'
formErrors.period = '璇烽€夋嫨鏃堕棿缁村害'
}
if (!reportForm.chartType) {
formErrors.chartType = '请选择图表类型'
formErrors.chartType = '璇烽€夋嫨鍥捐〃绫诲瀷'
}
if (formErrors.name || formErrors.description || formErrors.metrics || formErrors.period || formErrors.chartType) {
uni.showToast({ title: '请先修正表单中的错误提示', icon: 'none' })
uni.showToast({ title: '璇峰厛淇琛ㄥ崟涓殑閿欒鎻愮ず', icon: 'none' })
return false
}
return true
@@ -402,15 +402,15 @@ async function saveReport() {
}
try {
uni.showLoading({ title: '保存中...' })
uni.showLoading({ title: '淇濆瓨涓?..' })
await ensureSupabaseReady()
const uid = getUserIdOrNull()
if (!uid || uid.length === 0) {
uni.hideLoading()
uni.showModal({
title: '需要登录',
content: '创建自定义报表需要先登录,是否前往登录页面?',
title: '闇€瑕佺櫥褰?,
content: '鍒涘缓鑷畾涔夋姤琛ㄩ渶瑕佸厛鐧诲綍锛屾槸鍚﹀墠寰€鐧诲綍椤甸潰锛?,
success: (res) => {
if (res.confirm) {
goToLogin('/pages/mall/analytics/custom-report')
@@ -442,17 +442,17 @@ async function saveReport() {
uni.hideLoading()
// 检查 newReportId 是否有效,无效则认为创建失败
// 妫€鏌?newReportId 鏄惁鏈夋晥锛屾棤鏁堝垯璁や负鍒涘缓澶辫触
if (newReportId == null || newReportId.length === 0) {
uni.showToast({
title: '创建失败:未返回报表ID',
title: '鍒涘缓澶辫触锛氭湭杩斿洖鎶ヨ〃ID',
icon: 'none',
duration: 3000
})
return
}
uni.showToast({ title: '保存成功', icon: 'success' })
uni.showToast({ title: '淇濆瓨鎴愬姛', icon: 'success' })
closeModal()
loadReports()
@@ -466,7 +466,7 @@ async function saveReport() {
uni.hideLoading()
console.error('saveReport exception:', e)
uni.showToast({
title: mapAnalyticsError(e, { fallbackMessage: '保存失败' }),
title: mapAnalyticsError(e, { fallbackMessage: '淇濆瓨澶辫触' }),
icon: 'none',
duration: 3000
})
@@ -486,7 +486,7 @@ function closeModal() {
function refreshData() {
loadReports()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function handleMenu() {
@@ -506,31 +506,31 @@ function closeMoreMenu() {
}
function goToLogin() {
ensureAnalyticsLogin({ toastTitle: '请先登录后使用自定义报表' })
ensureAnalyticsLogin({ toastTitle: '璇峰厛鐧诲綍鍚庝娇鐢ㄨ嚜瀹氫箟鎶ヨ〃' })
}
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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
function handleGoToLogin() {
@@ -545,7 +545,7 @@ function handleGoToLogin() {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -557,18 +557,18 @@ function handleGoToLogin() {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -701,7 +701,7 @@ function handleGoToLogin() {
color: #111;
}
/* 工具栏 */
/* 宸ュ叿鏍?*/
.toolbar {
margin-top: 12px;
padding: 12px 16px;
@@ -752,7 +752,7 @@ function handleGoToLogin() {
opacity: 0.9;
}
/* 报表列表 */
/* 鎶ヨ〃鍒楄〃 */
.report-list {
margin-top: 12px;
display: flex;
@@ -858,7 +858,7 @@ function handleGoToLogin() {
text-align: center;
}
/* 模态框 */
/* 妯℃€佹 */
.modal {
position: fixed;
top: 0;
@@ -1009,7 +1009,7 @@ function handleGoToLogin() {
color: #fff;
}
/* 响应式 */
/* 鍝嶅簲寮?*/
@media screen and (max-width: 960px) {
.title,
.subtitle {
@@ -1030,7 +1030,7 @@ function handleGoToLogin() {
}
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -1041,3 +1041,4 @@ function handleGoToLogin() {
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<template>
<view class="page" @click="closeMoreMenu">
<!-- 固定顶部导航栏 -->
<!-- 鍥哄畾椤堕儴瀵艰埅鏍?-->
<AnalyticsTopBar
:title="'配送效率分析'"
:title="'閰嶉€佹晥鐜囧垎鏋?"
:lastUpdateTime="lastUpdateTime"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@@ -16,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,53 +54,53 @@
@clear="onDateRangeClear"
/>
<!-- KPI 指标卡片 -->
<!-- KPI 鎸囨爣鍗$墖 -->
<view class="kpi-grid">
<view class="kpi-card">
<text class="kpi-label">配送时效</text>
<text class="kpi-value">{{ deliveryData.avg_delivery_time }}分钟</text>
<text class="kpi-meta">较上期:{{ formatPct(deliveryData.time_growth) }}</text>
<text class="kpi-label">閰嶉€佹椂鏁?/text>
<text class="kpi-value">{{ deliveryData.avg_delivery_time }}鍒嗛挓</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(deliveryData.time_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">配送费用</text>
<text class="kpi-value">¥{{ formatMoney(deliveryData.total_fee) }}</text>
<text class="kpi-meta">平均:¥{{ formatMoney(deliveryData.avg_fee) }}</text>
<text class="kpi-label">閰嶉€佽垂鐢?/text>
<text class="kpi-value">{{ formatMoney(deliveryData.total_fee) }}</text>
<text class="kpi-meta">骞冲潎锛毬{ formatMoney(deliveryData.avg_fee) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">配送员效率</text>
<text class="kpi-label">閰嶉€佸憳鏁堢巼</text>
<text class="kpi-value">{{ formatInt(deliveryData.avg_orders_per_driver) }}</text>
<text class="kpi-meta">单/人/天</text>
<text class="kpi-meta">鍗?浜?澶?/text>
</view>
<view class="kpi-card">
<text class="kpi-label">客户满意度</text>
<text class="kpi-value">{{ formatScore(deliveryData.satisfaction_rate) }}分</text>
<text class="kpi-meta">较上期:{{ formatPct(deliveryData.satisfaction_growth) }}</text>
<text class="kpi-label">瀹㈡埛婊℃剰搴?/text>
<text class="kpi-value">{{ formatScore(deliveryData.satisfaction_rate) }}鍒?/text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(deliveryData.satisfaction_growth) }}</text>
</view>
</view>
<!-- 配送时效 & 满意度(合并图表) -->
<!-- 閰嶉€佹椂鏁?& 婊℃剰搴︼紙鍚堝苟鍥捐〃锛?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">配送时效 & 满意度</text>
<text class="card-desc">{{ selectedPeriodText }} · 平均配送时间 / 满意度趋势</text>
<text class="card-title">閰嶉€佹椂鏁?& 婊℃剰搴?/text>
<text class="card-desc">{{ selectedPeriodText }} 路 骞冲潎閰嶉€佹椂闂?/ 婊℃剰搴﹁秼鍔?/text>
</view>
<EChartsView class="chart-box" :option="timeChartOption" />
</view>
<!-- 配送费用分析 -->
<!-- 閰嶉€佽垂鐢ㄥ垎鏋?-->
<view class="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>
<EChartsView class="chart-box" :option="feeChartOption" />
</view>
<!-- 配送员效率排行 -->
<!-- 閰嶉€佸憳鏁堢巼鎺掕 -->
<view class="card">
<view class="card-head">
<text class="card-title">配送员效率排行 TOP 10</text>
<text class="card-desc">按订单数排序</text>
<text class="card-title">閰嶉€佸憳鏁堢巼鎺掕 TOP 10</text>
<text class="card-desc">鎸夎鍗曟暟鎺掑簭</text>
</view>
<view class="rank-scroll" @mouseenter="onRankHover(true)" @mouseleave="onRankHover(false)">
<scroll-view class="rank-scroll-inner" :scroll-y="true" :show-scrollbar="true">
@@ -110,9 +109,9 @@
<text class="rank-no">{{ d.rank }}</text>
<text class="rank-name">{{ d.name }}</text>
<view class="rank-right">
<text class="rank-val">{{ d.orders }} 单</text>
<text class="rank-val">{{ d.orders }} 鍗?/text>
<text class="chip" :class="d.rating >= 4.5 ? 'pos' : 'neg'">
⭐{{ d.rating }}
猸恵{ d.rating }}
</text>
</view>
</view>
@@ -121,7 +120,7 @@
</view>
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -156,10 +155,10 @@ const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/delivery-analysis')
const timePeriods = ref<Array<TimePeriod>>([
{ 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 deliveryData = reactive<DeliveryData>({
@@ -182,7 +181,7 @@ const _trendRows = ref<Array<UTSJSONObject>>([])
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
onLoad(() => {
@@ -269,7 +268,7 @@ async function loadDeliveryData() {
list.push({
id: r.getString('driver_id') ?? String(i),
rank: i + 1,
name: r.getString('driver_name') ?? '未知',
name: r.getString('driver_name') ?? '鏈煡',
orders: r.getNumber('orders') ?? 0,
rating: r.getNumber('rating_avg') ?? 0
})
@@ -282,7 +281,7 @@ async function loadDeliveryData() {
console.error('loadDeliveryData failed:', e)
updateTime()
buildChartOptions()
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '配送分析数据加载失败' }), icon: 'none' })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '閰嶉€佸垎鏋愭暟鎹姞杞藉け璐? }), icon: 'none' })
}
}
@@ -314,13 +313,13 @@ function onDateRangeClear() {
function refreshData() {
loadDeliveryData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -333,13 +332,13 @@ function updateTime() {
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(2)
}
@@ -374,7 +373,7 @@ function buildChartOptions() {
timeChartOption.value = {
tooltip: { trigger: 'axis' },
legend: {
data: ['平均配送时间(分钟)', '满意度(评分)'],
data: ['骞冲潎閰嶉€佹椂闂?鍒嗛挓)', '婊℃剰搴?璇勫垎)'],
top: 'bottom',
itemGap: 30,
itemWidth: 16,
@@ -389,13 +388,13 @@ function buildChartOptions() {
yAxis: [
{
type: 'value',
name: '配送时间',
name: '閰嶉€佹椂闂?,
min: 0,
splitLine: { lineStyle: { color: '#e5e7eb' } }
},
{
type: 'value',
name: '满意度',
name: '婊℃剰搴?,
min: 0,
max: 5,
position: 'right',
@@ -404,7 +403,7 @@ function buildChartOptions() {
],
series: [
{
name: '平均配送时间(分钟)',
name: '骞冲潎閰嶉€佹椂闂?鍒嗛挓)',
type: 'line',
smooth: true,
symbolSize: 6,
@@ -412,7 +411,7 @@ function buildChartOptions() {
yAxisIndex: 0
},
{
name: '满意度(评分)',
name: '婊℃剰搴?璇勫垎)',
type: 'line',
smooth: true,
symbol: 'circle',
@@ -430,7 +429,7 @@ function buildChartOptions() {
yAxis: { type: 'value' },
series: [
{
name: '平均配送费(元)',
name: '骞冲潎閰嶉€佽垂(鍏?',
type: 'bar',
data: feeSeries
}
@@ -455,27 +454,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
function onRankHover(hover: boolean) {
@@ -489,7 +488,7 @@ function onRankHover(hover: boolean) {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -501,18 +500,18 @@ function onRankHover(hover: boolean) {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -645,7 +644,7 @@ function onRankHover(hover: boolean) {
color: #111;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
margin-top: 12px;
display: flex;
@@ -675,7 +674,7 @@ function onRankHover(hover: boolean) {
color: #fff;
}
/* KPI 网格 */
/* KPI 缃戞牸 */
.kpi-grid {
margin-top: 12px;
display: flex;
@@ -712,7 +711,7 @@ function onRankHover(hover: boolean) {
color: rgba(0,0,0,0.55);
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -874,3 +873,4 @@ function onRankHover(hover: boolean) {
}
}
</style>

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,8 +92,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -104,15 +103,15 @@
@clear="onDateRangeClear"
/>
<!-- 核心趋势:占满横向(柱+折 组合图) -->
<!-- 鏍稿績瓒嬪娍锛氬崰婊℃í鍚戯紙鏌?鎶?缁勫悎鍥撅級 -->
<view class="card card-full">
<view class="card-head">
<text class="card-title">核心趋势GMV / 订单数)</text>
<text class="card-desc">{{ selectedPeriodText }} · 柱GMV · 线:订单数</text>
<text class="card-title">鏍稿績瓒嬪娍锛圙MV / 璁㈠崟鏁帮級</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
@@ -125,16 +124,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
@@ -144,14 +143,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
@@ -162,13 +161,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 }">
@@ -176,24 +175,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>
<!-- 右侧商家排行TOP -->
<!-- 鍙充晶锛氬晢瀹舵帓琛孴OP -->
<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 }">
@@ -202,18 +201,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>
@@ -225,47 +224,47 @@
</view>
</view>
<!-- 快速工具卡片区6个工具入口 -->
<!-- 蹇€熷伐鍏峰崱鐗囧尯锛?涓伐鍏峰叆鍙o級 -->
<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">发放、使用、ROI</text>
<view class="tool-icon coupon">馃帿</view>
<text class="tool-title">浼樻儬鍒稿垎鏋?/text>
<text class="tool-desc">鍙戞斁銆佷娇鐢ㄣ€丷OI</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>
@@ -302,10 +301,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({
@@ -329,7 +328,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() {
@@ -364,7 +363,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 = []
@@ -382,7 +381,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)
}
}
@@ -394,7 +393,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)
}
}
@@ -407,7 +406,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)
}
}
@@ -420,7 +419,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)
}
}
@@ -433,7 +432,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)
}
}
@@ -469,12 +468,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
}
@@ -485,15 +484,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 {
@@ -505,7 +504,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: {
@@ -547,7 +546,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),
@@ -557,10 +556,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 } },
@@ -570,7 +569,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}%' }
}
]
@@ -610,14 +609,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()
}
@@ -628,7 +627,7 @@ watch(
watch(
userSegments,
(newVal) => {
console.log('👀 watch userSegments 触发', newVal)
console.log('馃憖 watch userSegments 瑙﹀彂', newVal)
if (newVal && newVal.length > 0) {
buildChartOptions()
}
@@ -649,26 +648,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' })
}
}
@@ -687,7 +686,7 @@ async function initDashboard() {
async function selectPeriod(p: string) {
selectedPeriod.value = p
// 切换到快捷时间段时,退出自定义范围
// 鍒囨崲鍒板揩鎹锋椂闂存鏃讹紝閫€鍑鸿嚜瀹氫箟鑼冨洿
customRangeEnabled.value = false
selectedStartDate.value = ''
selectedEndDate.value = ''
@@ -700,19 +699,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
}
@@ -731,30 +730,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() {
@@ -777,13 +776,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)
}
@@ -845,22 +844,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()
})
@@ -881,15 +880,15 @@ onHide(() => {
</script>
<style>
/* 页面:白底 + 宽屏居中 + 自适应 */
/* 说明uni-app rpx 会随屏宽缩放,宽屏 H5 建议用 max-width 控制内容宽度。 */
/* 椤甸潰锛氱櫧搴?+ 瀹藉睆灞呬腑 + 鑷€傚簲 */
/* 璇存槑锛歶ni-app 鐨?rpx 浼氶殢灞忓缂╂斁锛屽灞?H5 寤鸿鐢?max-width 鎺у埗鍐呭瀹藉害銆?*/
.page {
min-height: 100vh;
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -901,19 +900,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: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
flex: 1;
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -924,8 +923,8 @@ onHide(() => {
}
}
/* 顶部 */
/* ✅ 强制:顶部必须横排(避免被全局 view:flex-direction:column 影响) */
/* 椤堕儴 */
/* 鉁?寮哄埗锛氶《閮ㄥ繀椤绘í鎺掞紙閬垮厤琚叏灞€ view:flex-direction:column 褰卞搷锛?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -973,13 +972,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 {
@@ -1001,7 +1000,7 @@ onHide(() => {
white-space: nowrap;
}
/* ✅ 右侧按钮永不换成竖列(必要时只裁切,不换行) */
/* 鉁?鍙充晶鎸夐挳姘镐笉鎹㈡垚绔栧垪锛堝繀瑕佹椂鍙鍒囷紝涓嶆崲琛岋級 */
.topbar-right {
display: flex;
flex-direction: row !important;
@@ -1026,7 +1025,7 @@ onHide(() => {
color: #fff;
}
/* 图标按钮样式 */
/* 鍥炬爣鎸夐挳鏍峰紡 */
.icon-btn-icon {
width: 32px;
height: 32px;
@@ -1050,7 +1049,7 @@ onHide(() => {
line-height: 1;
}
/* 通知图标带红点 */
/* 閫氱煡鍥炬爣甯︾孩鐐?*/
.icon-btn-icon.notification .badge {
position: absolute;
top: 4px;
@@ -1062,7 +1061,7 @@ onHide(() => {
border: 1px solid #fff;
}
/* 下拉菜单 */
/* 涓嬫媺鑿滃崟 */
.dropdown {
display: flex;
flex-direction: row !important;
@@ -1077,7 +1076,7 @@ onHide(() => {
flex-shrink: 0;
}
/* 更多按钮(默认隐藏,窄屏时显示) */
/* 鏇村鎸夐挳锛堥粯璁ら殣钘忥紝绐勫睆鏃舵樉绀猴級 */
.more-btn {
display: none;
width: 32px;
@@ -1102,7 +1101,7 @@ onHide(() => {
color: #111;
}
/* 更多菜单下拉 */
/* 鏇村鑿滃崟涓嬫媺 */
.more-menu {
position: absolute;
top: calc(100% + 8px);
@@ -1157,8 +1156,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;
@@ -1187,7 +1186,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;
@@ -1269,7 +1268,7 @@ onHide(() => {
color: #fff;
}
/* 时间维度 tabs 横排 */
/* 鏃堕棿缁村害 tabs 妯帓 */
.tabs {
margin-top: 12px;
display: flex;
@@ -1299,7 +1298,7 @@ onHide(() => {
color: #fff;
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -1336,17 +1335,17 @@ onHide(() => {
color: rgba(0,0,0,0.55);
}
/* 图表必须给高度H5 否则可能 0 高) */
/* 鍥捐〃蹇呴』缁欓珮搴︼紙H5 鍚﹀垯鍙兘 0 楂橈級 */
.chart-box {
width: 100%;
height: 360px; /* 建议用 pxH5 更稳 */
height: 360px; /* 寤鸿鐢?px锛孒5 鏇寸ǔ */
}
.fullwide .chart-box {
height: 420px; /* 大图更高 */
height: 420px; /* 澶у浘鏇撮珮 */
}
/* 图表加载状态 */
/* 鍥捐〃鍔犺浇鐘舵€?*/
.chart-loading {
width: 100%;
height: 360px;
@@ -1361,7 +1360,7 @@ onHide(() => {
height: 420px;
}
/* 用户结构和流量来源:横排显示 */
/* 鐢ㄦ埛缁撴瀯鍜屾祦閲忔潵婧愶細妯帓鏄剧ず */
.charts-row {
display: flex;
flex-direction: row !important;
@@ -1377,7 +1376,7 @@ onHide(() => {
min-width: 360px;
}
/* 两个TOP排行横排显示 */
/* 涓や釜TOP鎺掕锛氭í鎺掓樉绀?*/
.tops-row {
display: flex;
flex-direction: row !important;
@@ -1393,7 +1392,7 @@ onHide(() => {
min-width: 360px;
}
/* 滚动容器 */
/* 婊氬姩瀹瑰櫒 */
.rank-scroll-container {
width: 100%;
height: 300px;
@@ -1409,7 +1408,7 @@ onHide(() => {
animation: scrollRank 15s linear infinite;
}
/* 列表样式 */
/* 鍒楄〃鏍峰紡 */
.rank-list {
display: flex;
flex-direction: column;
@@ -1417,14 +1416,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%));
}
}
@@ -1487,14 +1486,14 @@ onHide(() => {
color: #dc2626;
}
/* 宽屏KPI 4*/
/* 瀹藉睆锛欿PI 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;
}
@@ -1503,13 +1502,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 {
@@ -1529,22 +1528,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;
@@ -1569,18 +1568,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;
}
@@ -1680,3 +1679,4 @@ onHide(() => {
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<template>
<view class="page" @click="closeMoreMenu">
<AnalyticsTopBar
:title="'数据洞察详情'"
:title="'鏁版嵁娲炲療璇︽儏'"
:lastUpdateTime="lastUpdateTime"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@@ -25,7 +25,7 @@
<view class="container">
<view class="card card-full">
<view class="card-head">
<text class="card-title">{{ insight.title || '洞察详情' }}</text>
<text class="card-title">{{ insight.title || '娲炲療璇︽儏' }}</text>
<view class="meta-row">
<text class="badge" :class="'badge-' + (insight.type || 'info')">{{ getInsightTypeText(insight.type) }}</text>
<text class="badge badge-impact" :class="'impact-' + (insight.impact || 'medium')">{{ getImpactText(insight.impact) }}</text>
@@ -34,7 +34,7 @@
</view>
<view v-if="loading" class="state">
<text class="state-text">加载中...</text>
<text class="state-text">鍔犺浇涓?..</text>
</view>
<view v-else-if="errorMsg" class="state">
<text class="state-text">{{ errorMsg }}</text>
@@ -46,11 +46,11 @@
<view class="card" v-if="relatedReport.id">
<view class="card-head">
<text class="card-title">关联报表</text>
<text class="card-desc">{{ relatedReport.type }} · {{ relatedReport.period }}</text>
<text class="card-title">鍏宠仈鎶ヨ〃</text>
<text class="card-desc">{{ relatedReport.type }} {{ relatedReport.period }}</text>
</view>
<view class="report-row" @click="goToReportDetail">
<view class="report-icon">📄</view>
<view class="report-icon">馃搫</view>
<view class="report-info">
<text class="report-title">{{ relatedReport.title }}</text>
<text class="report-time">{{ relatedReport.generated_at ? formatTime(relatedReport.generated_at) : '' }}</text>
@@ -110,7 +110,7 @@ onLoad((options: any) => {
const iid = (options.insightId || options.id) as string
if (!iid) {
uni.showToast({ title: '缺少洞察ID', icon: 'none' })
uni.showToast({ title: '缂哄皯娲炲療ID', icon: 'none' })
setTimeout(() => uni.navigateBack(), 1500)
return
}
@@ -131,7 +131,7 @@ async function loadInsightDetail() {
const data = await fetchInsightDetail(insightId.value)
if (data == null) {
errorMsg.value = '洞察不存在或无权限访问'
errorMsg.value = '娲炲療涓嶅瓨鍦ㄦ垨鏃犳潈闄愯闂?
return
}
@@ -165,7 +165,7 @@ async function loadInsightDetail() {
}
} catch (e) {
console.error('loadInsightDetail failed', e)
errorMsg.value = mapAnalyticsError(e, { fallbackMessage: '加载失败,请稍后重试' })
errorMsg.value = mapAnalyticsError(e, { fallbackMessage: '鍔犺浇澶辫触锛岃绋嶅悗閲嶈瘯' })
} finally {
loading.value = false
}
@@ -173,13 +173,13 @@ async function loadInsightDetail() {
function refreshData() {
void loadInsightDetail()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -198,21 +198,21 @@ function formatTime(timeStr: string): string {
function getInsightTypeText(type: string): string {
const t = `${type || 'info'}`
const map: Record<string, string> = {
positive: '正向',
warning: '预警',
negative: '风险',
info: '信息'
positive: '姝e悜',
warning: '棰勮',
negative: '椋庨櫓',
info: '淇℃伅'
}
return map[t] || '信息'
return map[t] || '淇℃伅'
}
function getImpactText(impact: string): string {
const impacts: Record<string, string> = {
high: '高影响',
medium: '中影响',
low: '低影响'
high: '楂樺奖鍝?,
medium: '涓奖鍝?,
low: '浣庡奖鍝?
}
return impacts[impact || 'medium'] || '中影响'
return impacts[impact || 'medium'] || '涓奖鍝?
}
function goToReportDetail() {
@@ -239,27 +239,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
</script>
@@ -287,7 +287,8 @@ function handleSettings() {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
</style>

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,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,52 +54,52 @@
@clear="onDateRangeClear"
/>
<!-- 市场整体趋势 -->
<!-- 甯傚満鏁翠綋瓒嬪娍 -->
<view class="card card-full">
<view class="card-head">
<text class="card-title">市场整体趋势</text>
<text class="card-desc">{{ selectedPeriodText }} · GMV、订单数、用户数</text>
<text class="card-title">甯傚満鏁翠綋瓒嬪娍</text>
<text class="card-desc">{{ selectedPeriodText }} GMV銆佽鍗曟暟銆佺敤鎴锋暟</text>
</view>
<EChartsView class="chart-box" :option="marketTrendOption" />
</view>
<!-- 行业对比分析 -->
<!-- 琛屼笟瀵规瘮鍒嗘瀽 -->
<view class="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>
<EChartsView class="chart-box" :option="industryCompareOption" />
</view>
<!-- 季节性趋势 -->
<!-- 瀛h妭鎬ц秼鍔?-->
<view class="card">
<view class="card-head">
<text class="card-title">季节性趋势</text>
<text class="card-desc">按月份统计</text>
<text class="card-title">瀛h妭鎬ц秼鍔?/text>
<text class="card-desc">鎸夋湀浠界粺璁?/text>
</view>
<EChartsView class="chart-box" :option="seasonalTrendOption" />
</view>
<!-- 价格趋势分析 -->
<!-- 浠锋牸瓒嬪娍鍒嗘瀽 -->
<view class="card card-full">
<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>
<EChartsView class="chart-box" :option="priceTrendOption" />
</view>
<!-- 竞争分析 -->
<!-- 绔炰簤鍒嗘瀽 -->
<view class="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>
<EChartsView class="chart-box" :option="competitionOption" />
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -135,10 +134,10 @@ const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/market-trends')
const timePeriods = ref<Array<TimePeriod>>([
{ 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 marketTrendOption = ref<any>({})
@@ -155,7 +154,7 @@ const _competitionRows = ref<any>(null)
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
onLoad(() => {
@@ -188,7 +187,7 @@ async function loadMarketData() {
console.error('loadMarketData failed:', e)
updateTime()
buildChartOptions()
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '市场趋势数据加载失败' }), icon: 'none' })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '甯傚満瓒嬪娍鏁版嵁鍔犺浇澶辫触' }), icon: 'none' })
}
}
@@ -202,7 +201,7 @@ function selectPeriod(p: string) {
function refreshData() {
loadMarketData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function toggleCustomRange() {
@@ -225,8 +224,8 @@ function onDateRangeClear() {
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -250,8 +249,7 @@ function buildChartOptions() {
const priceRows = Array.isArray(priceAny) ? (priceAny as Array<UTSJSONObject>) : []
const compRows = Array.isArray(compAny) ? (compAny as Array<UTSJSONObject>) : []
// 1) 市场整体趋势GMV / 订单数 / 用户数
const mtDays: string[] = []
// 1) 甯傚満鏁翠綋瓒嬪娍锛欸MV / 璁㈠崟鏁?/ 鐢ㄦ埛鏁? const mtDays: string[] = []
const mtGmv: number[] = []
const mtOrders: number[] = []
const mtUsers: number[] = []
@@ -268,14 +266,14 @@ function buildChartOptions() {
marketTrendOption.value = {
tooltip: { trigger: 'axis' },
legend: {
data: ['GMV', '订单数', '用户数'],
data: ['GMV', '璁㈠崟鏁?, '鐢ㄦ埛鏁?],
top: 'bottom'
},
grid: { left: 50, right: 60, top: 40, bottom: 60 },
xAxis: { type: 'category', data: mtDays },
yAxis: [
{ type: 'value', name: 'GMV', splitLine: { lineStyle: { color: '#e5e7eb' } } },
{ type: 'value', name: '数量', position: 'right', splitLine: { show: false } }
{ type: 'value', name: '鏁伴噺', position: 'right', splitLine: { show: false } }
],
series: [
{
@@ -286,14 +284,14 @@ function buildChartOptions() {
itemStyle: { color: '#3b82f6' }
},
{
name: '订单数',
name: '璁㈠崟鏁?,
type: 'line',
yAxisIndex: 1,
smooth: true,
data: mtOrders
},
{
name: '用户数',
name: '鐢ㄦ埛鏁?,
type: 'line',
yAxisIndex: 1,
smooth: true,
@@ -302,12 +300,12 @@ function buildChartOptions() {
]
}
// 2) 行业对比:分类 GMV
// 2) 琛屼笟瀵规瘮锛氬垎绫?GMV
const catNames: string[] = []
const catSales: number[] = []
for (let i = 0; i < industryRows.length; i++) {
const r = industryRows[i]
catNames.push(r.getString('category_name') ?? '未分类')
catNames.push(r.getString('category_name') ?? '鏈垎绫?)
catSales.push(r.getNumber('total_sales') ?? 0)
}
@@ -325,7 +323,7 @@ function buildChartOptions() {
]
}
// 3) 季节性趋势:按月 GMV
// 3) 瀛h妭鎬ц秼鍔匡細鎸夋湀 GMV
const seaMonths: string[] = []
const seaGmv: number[] = []
for (let i = 0; i < seasonalRows.length; i++) {
@@ -349,8 +347,7 @@ function buildChartOptions() {
]
}
// 4) 价格趋势:按天平均价格
const priceDays: string[] = []
// 4) 浠锋牸瓒嬪娍锛氭寜澶╁钩鍧囦环鏍? const priceDays: string[] = []
const avgPrices: number[] = []
for (let i = 0; i < priceRows.length; i++) {
const r = priceRows[i]
@@ -363,10 +360,10 @@ function buildChartOptions() {
tooltip: { trigger: 'axis' },
grid: { left: 50, right: 20, top: 30, bottom: 60 },
xAxis: { type: 'category', data: priceDays },
yAxis: { type: 'value', name: '平均价格' },
yAxis: { type: 'value', name: '骞冲潎浠锋牸' },
series: [
{
name: '平均价格',
name: '骞冲潎浠锋牸',
type: 'line',
smooth: true,
data: avgPrices
@@ -374,12 +371,12 @@ function buildChartOptions() {
]
}
// 5) 竞争分析:商家 GMV 对比
// 5) 绔炰簤鍒嗘瀽锛氬晢瀹?GMV 瀵规瘮
const merchantNames: string[] = []
const merchantGmv: number[] = []
for (let i = 0; i < compRows.length; i++) {
const r = compRows[i]
merchantNames.push(r.getString('merchant_name') ?? '未知商家')
merchantNames.push(r.getString('merchant_name') ?? '鏈煡鍟嗗')
merchantGmv.push(r.getNumber('gmv') ?? 0)
}
@@ -388,7 +385,7 @@ function buildChartOptions() {
legend: { top: 'bottom' },
series: [
{
name: '商家GMV',
name: '鍟嗗GMV',
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '50%'],
@@ -417,27 +414,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
</script>
@@ -447,7 +444,7 @@ function handleSettings() {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -459,18 +456,18 @@ function handleSettings() {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -603,7 +600,7 @@ function handleSettings() {
color: #111;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
margin-top: 12px;
display: flex;
@@ -633,7 +630,7 @@ function handleSettings() {
color: #fff;
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -687,7 +684,7 @@ function handleSettings() {
}
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -698,3 +695,4 @@ function handleSettings() {
}
}
</style>

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,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,34 +54,34 @@
@clear="onDateRangeClear"
/>
<!-- KPI 指标卡片 -->
<!-- KPI 鎸囨爣鍗$墖 -->
<view class="kpi-grid">
<view class="kpi-card">
<text class="kpi-label">商品总数</text>
<text class="kpi-label">鍟嗗搧鎬绘暟</text>
<text class="kpi-value">{{ formatInt(productData.total_products) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(productData.product_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(productData.product_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">热销商品</text>
<text class="kpi-label">鐑攢鍟嗗搧</text>
<text class="kpi-value">{{ formatInt(productData.hot_products) }}</text>
<text class="kpi-meta">销量 &gt; 100</text>
<text class="kpi-meta">閿€閲?&gt; 100</text>
</view>
<view class="kpi-card">
<text class="kpi-label">库存周转率</text>
<text class="kpi-label">搴撳瓨鍛ㄨ浆鐜?/text>
<text class="kpi-value">{{ formatPct(productData.turnover_rate) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(productData.turnover_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(productData.turnover_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">平均库存</text>
<text class="kpi-label">骞冲潎搴撳瓨</text>
<text class="kpi-value">{{ formatInt(productData.avg_stock) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(productData.stock_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(productData.stock_growth) }}</text>
</view>
</view>
<!-- 商品销售分析 -->
<!-- 鍟嗗搧閿€鍞垎鏋?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">商品销售分析</text>
<text class="card-title">鍟嗗搧閿€鍞垎鏋?/text>
<view class="card-head-right">
<select class="select" v-model="selectedProductId" @change="handleProductChange">
<option v-for="p in topProducts" :key="p.id" :value="p.id">{{ p.name }}</option>
@@ -90,33 +89,33 @@
</view>
</view>
<view v-if="loading || !salesChartOption || !salesChartOption.series || salesChartOption.series.length === 0" class="chart-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box" :option="salesChartOption" />
</view>
<!-- 第二行:分类 & 排行 -->
<!-- 绗簩琛岋細鍒嗙被 & 鎺掕 -->
<view class="grid-row">
<!-- 商品分类分析 -->
<!-- 鍟嗗搧鍒嗙被鍒嗘瀽 -->
<view class="card grid-col-item">
<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 || !categoryChartOption || !categoryChartOption.series || categoryChartOption.series.length === 0" class="chart-loading chart-loading-sm">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box chart-box-sm" :option="categoryChartOption" />
</view>
<!-- 热销商品排行 -->
<!-- 鐑攢鍟嗗搧鎺掕 -->
<view class="card grid-col-item">
<view class="card-head">
<text class="card-title">热销商品排行 TOP 10</text>
<text class="card-desc">按销量排序</text>
<text class="card-title">鐑攢鍟嗗搧鎺掕 TOP 10</text>
<text class="card-desc">鎸夐攢閲忔帓搴?/text>
</view>
<view v-if="loading || topProducts.length === 0" class="chart-loading chart-loading-sm">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<view v-else class="rank-list-scroll">
<view class="rank-list">
@@ -124,7 +123,7 @@
<text class="rank-no">{{ p.rank }}</text>
<text class="rank-name">{{ p.name }}</text>
<view class="rank-right">
<text class="rank-val">{{ p.sales }} 件</text>
<text class="rank-val">{{ p.sales }} 浠?/text>
<text class="chip" :class="p.growth >= 0 ? 'pos' : 'neg'">
{{ p.growth >= 0 ? '+' : '' }}{{ p.growth }}%
</text>
@@ -135,46 +134,46 @@
</view>
</view>
<!-- 第三行:库存 & 价格 -->
<!-- 绗笁琛岋細搴撳瓨 & 浠锋牸 -->
<view class="grid-row">
<!-- 商品库存分析 -->
<!-- 鍟嗗搧搴撳瓨鍒嗘瀽 -->
<view class="card grid-col-item">
<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 || !stockChartOption || !stockChartOption.series || stockChartOption.series.length === 0" class="chart-loading chart-loading-sm">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box chart-box-sm" :option="stockChartOption" />
</view>
<!-- 商品价格趋势 -->
<!-- 鍟嗗搧浠锋牸瓒嬪娍 -->
<view class="card grid-col-item">
<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 || !priceChartOption || !priceChartOption.series || priceChartOption.series.length === 0" class="chart-loading chart-loading-sm">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box chart-box-sm" :option="priceChartOption" />
</view>
</view>
<!-- 第四行:评价 -->
<!-- 绗洓琛岋細璇勪环 -->
<view class="card card-full">
<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 || !reviewChartOption || !reviewChartOption.series || reviewChartOption.series.length === 0" class="chart-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box" :option="reviewChartOption" />
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -208,10 +207,10 @@ const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/product-insights')
const timePeriods = ref<Array<TimePeriod>>([
{ 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 productData = reactive<ProductData>({
@@ -237,7 +236,7 @@ const loading = ref(false)
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
onLoad(() => {
@@ -254,7 +253,7 @@ function updateTime() {
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()
}
@@ -295,16 +294,16 @@ async function loadSelectedProductTrend() {
salesChartOption.value = {
grid: { left: 50, right: 50, top: 20, bottom: 46 },
tooltip: { trigger: 'axis' },
legend: { data: ['GMV', '件数', '订单数'], bottom: 0 },
legend: { data: ['GMV', '浠舵暟', '璁㈠崟鏁?], bottom: 0 },
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
yAxis: [
{ type: 'value', name: 'GMV', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
{ type: 'value', name: '件/单', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { show: false } }
{ type: 'value', name: '浠?鍗?, axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { show: false } }
],
series: [
{ name: 'GMV', type: 'bar', data: gmv, barWidth: 14, itemStyle: { borderRadius: 6 } },
{ name: '件数', type: 'line', yAxisIndex: 1, data: qty, smooth: true, symbolSize: 6 },
{ name: '订单数', type: 'line', yAxisIndex: 1, data: orders, smooth: true, symbolSize: 6 }
{ name: '浠舵暟', type: 'line', yAxisIndex: 1, data: qty, smooth: true, symbolSize: 6 },
{ name: '璁㈠崟鏁?, type: 'line', yAxisIndex: 1, data: orders, smooth: true, symbolSize: 6 }
]
}
@@ -319,13 +318,13 @@ async function loadSelectedProductTrend() {
grid: { left: 40, right: 18, top: 20, bottom: 40 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
yAxis: { type: 'value', name: '均价', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
series: [{ name: '均价', type: 'line', data: avgPrice, smooth: true, symbolSize: 6, color: '#f97316' }]
yAxis: { type: 'value', name: '鍧囦环', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
series: [{ name: '鍧囦环', type: 'line', data: avgPrice, smooth: true, symbolSize: 6, color: '#f97316' }]
}
} catch (e) {
console.error('loadSelectedProductTrend failed', e)
salesChartOption.value = {}
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '加载商品趋势失败' }), icon: 'none' })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '鍔犺浇鍟嗗搧瓒嬪娍澶辫触' }), icon: 'none' })
}
}
@@ -338,7 +337,7 @@ function buildCategoryChart(catRows: any) {
const names: Array<string> = []
const values: Array<number> = []
for (let i = 0; i < rows.length; i++) {
names.push(`${rows[i].category_name ?? '未分类'}`)
names.push(`${rows[i].category_name ?? '鏈垎绫?}`)
values.push(Number(rows[i].total_sales) || 0)
}
@@ -435,7 +434,7 @@ async function loadProductData() {
updateTime()
} catch (e) {
console.error('loadProductData failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '商品洞察数据加载失败' }), icon: 'none', duration: 2000 })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '鍟嗗搧娲炲療鏁版嵁鍔犺浇澶辫触' }), icon: 'none', duration: 2000 })
} finally {
loading.value = false
updateTime()
@@ -470,13 +469,13 @@ function onDateRangeClear() {
function refreshData() {
loadProductData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -497,27 +496,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
</script>
@@ -527,7 +526,7 @@ function handleSettings() {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -539,18 +538,18 @@ function handleSettings() {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -684,7 +683,7 @@ function handleSettings() {
color: #111;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
margin-top: 12px;
display: flex;
@@ -714,7 +713,7 @@ function handleSettings() {
color: #fff;
}
/* KPI 网格 */
/* KPI 缃戞牸 */
.kpi-grid {
margin-top: 12px;
display: flex;
@@ -751,7 +750,7 @@ function handleSettings() {
color: rgba(0,0,0,0.55);
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -929,7 +928,7 @@ function handleSettings() {
}
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -940,3 +939,4 @@ function handleSettings() {
}
}
</style>

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,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,38 +54,38 @@
@clear="onDateRangeClear"
/>
<!-- KPI 指标卡片 -->
<!-- KPI 鎸囨爣鍗$墖 -->
<view class="kpi-grid">
<view class="kpi-card">
<text class="kpi-label">GMV(成交总额)</text>
<text class="kpi-value">¥{{ formatMoney(salesData.gmv) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(salesData.gmv_growth) }}</text>
<text class="kpi-label">GMV锛堟垚浜ゆ€婚锛?/text>
<text class="kpi-value">{{ formatMoney(salesData.gmv) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(salesData.gmv_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">订单量</text>
<text class="kpi-label">璁㈠崟閲?/text>
<text class="kpi-value">{{ formatInt(salesData.orders) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(salesData.order_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(salesData.order_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">转化率</text>
<text class="kpi-label">杞寲鐜?/text>
<text class="kpi-value">{{ formatPct(salesData.conversion_rate) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(salesData.conversion_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(salesData.conversion_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">客单价</text>
<text class="kpi-value">¥{{ formatMoney(salesData.avg_order_amount) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(salesData.avg_order_growth) }}</text>
<text class="kpi-label">瀹㈠崟浠?/text>
<text class="kpi-value">{{ formatMoney(salesData.avg_order_amount) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(salesData.avg_order_growth) }}</text>
</view>
</view>
<!-- 销售趋势图表 -->
<!-- 閿€鍞秼鍔垮浘琛?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">销售趋势分析</text>
<text class="card-desc">{{ selectedPeriodText }} · 柱GMV · 线:订单数</text>
<text class="card-title">閿€鍞秼鍔垮垎鏋?/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>
<AnalyticsComboChart
v-else
@@ -97,7 +96,7 @@
/>
</view>
<!-- 销售地域分布(左地图 + 右双列表,同一块) -->
<!-- 閿€鍞湴鍩熷垎甯冿紙宸﹀湴鍥?+ 鍙冲弻鍒楄〃锛屽悓涓€鍧楋級 -->
<view class="card card-full sales-overview-card">
<view class="sales-split">
<view class="sales-split-left">
@@ -111,18 +110,18 @@
<view class="sales-split-right">
<view class="sales-split-list">
<view class="list-head">
<text class="list-title">商品销售排行 TOP 10</text>
<text class="list-desc">按销量排序</text>
<text class="list-title">鍟嗗搧閿€鍞帓琛?TOP 10</text>
<text class="list-desc">鎸夐攢閲忔帓搴?/text>
</view>
<view v-if="loading || topProducts.length === 0" class="chart-loading chart-loading-compact">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<view v-else class="rank-scroll">
<view class="rank-list">
<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>
</view>
@@ -130,11 +129,11 @@
<view class="sales-split-list">
<view class="list-head">
<text class="list-title">商家销售排行 TOP 10</text>
<text class="list-desc">GMV 排序</text>
<text class="list-title">鍟嗗閿€鍞帓琛?TOP 10</text>
<text class="list-desc">鎸?GMV 鎺掑簭</text>
</view>
<view v-if="loading || topMerchants.length === 0" class="chart-loading chart-loading-compact">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<view v-else class="rank-scroll">
<view class="rank-list">
@@ -142,7 +141,7 @@
<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>
@@ -155,7 +154,7 @@
</view>
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -189,10 +188,10 @@ const currentPath = ref('/pages/mall/analytics/sales-report')
const loading = ref(false)
const timePeriods = ref<Array<TimePeriod>>([
{ 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 salesData = reactive<SalesData>({
@@ -212,7 +211,7 @@ const topMerchants = reactive<Array<MerchantRank>>([])
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
onLoad(() => {
@@ -248,13 +247,13 @@ async function loadSalesData() {
salesData.avg_order_amount = kpi.avg_order_amount
salesData.avg_order_growth = kpi.avg_order_growth
// 趋势
// 瓒嬪娍
const t = await fetchSalesTrend(selectedPeriod.value, range)
trend.x = t.x
trend.gmv = t.gmv
trend.orders = t.orders
// TOP 商品/商家
// TOP 鍟嗗搧/鍟嗗
const pList = await fetchSalesTopProducts(selectedPeriod.value, 50, range)
for (let i = 0; i < pList.length; i++) {
pList[i].rank = i + 1
@@ -267,8 +266,8 @@ async function loadSalesData() {
}
topMerchants.splice(0, topMerchants.length, ...mList)
} catch (e) {
console.error('loadSalesData failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '数据加载失败' }), icon: 'none', duration: 2000 })
console.error('鉂?loadSalesData failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '鏁版嵁鍔犺浇澶辫触' }), icon: 'none', duration: 2000 })
} finally {
loading.value = false
updateTime()
@@ -285,13 +284,13 @@ function selectPeriod(p: string) {
function refreshData() {
loadSalesData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
@@ -304,13 +303,13 @@ function updateTime() {
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)
}
@@ -337,27 +336,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
function toggleCustomRange() {
@@ -385,7 +384,7 @@ function onDateRangeClear() {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -397,18 +396,18 @@ function onDateRangeClear() {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
.container {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 顶部栏 */
/* 椤堕儴鏍?*/
.topbar {
display: flex;
flex-direction: row !important;
@@ -542,7 +541,7 @@ function onDateRangeClear() {
color: #111;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
margin-top: 12px;
display: flex;
@@ -572,7 +571,7 @@ function onDateRangeClear() {
color: #fff;
}
/* KPI 网格 */
/* KPI 缃戞牸 */
.kpi-grid {
margin-top: 12px;
display: flex;
@@ -609,7 +608,7 @@ function onDateRangeClear() {
color: rgba(0,0,0,0.55);
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -729,7 +728,7 @@ function onDateRangeClear() {
}
}
/* 排行列表 */
/* 鎺掕鍒楄〃 */
.rank-scroll {
flex: 1;
min-height: 0;
@@ -738,14 +737,14 @@ function onDateRangeClear() {
padding-right: 6px;
-webkit-overflow-scrolling: touch;
/* 防止滚动链把滚轮事件传给页面 */
/* 闃叉婊氬姩閾炬妸婊氳疆浜嬩欢浼犵粰椤甸潰 */
overscroll-behavior: contain;
scroll-behavior: auto;
/* 默认隐藏滚动条(Firefox */
/* 榛樿闅愯棌婊氬姩鏉★紙Firefox锛?*/
scrollbar-width: none;
/* 默认隐藏滚动条(WebKit */
/* 榛樿闅愯棌婊氬姩鏉★紙WebKit锛?*/
-ms-overflow-style: none;
}
@@ -754,7 +753,7 @@ function onDateRangeClear() {
height: 0;
}
/* 鼠标悬停在方块内时显示滚动条,并允许拖动 */
/* 榧犳爣鎮仠鍦ㄦ柟鍧楀唴鏃舵樉绀烘粴鍔ㄦ潯锛屽苟鍏佽鎷栧姩 */
.sales-split-list:hover .rank-scroll {
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.35) rgba(0,0,0,0.06);
@@ -842,7 +841,7 @@ function onDateRangeClear() {
color: #dc2626;
}
/* 响应式 */
/* 鍝嶅簲寮?*/
@media screen and (min-width: 960px) {
.kpi-card {
flex: 1 1 calc(25% - 9px);
@@ -866,3 +865,4 @@ function onDateRangeClear() {
}
</style>

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,21 +16,21 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 全局筛选区 -->
<!-- 鍏ㄥ眬绛涢€夊尯 -->
<view class="filters">
<view class="filter-block">
<text class="filter-label">时间范围</text>
<text class="filter-label">鏃堕棿鑼冨洿</text>
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -46,8 +46,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
</view>
@@ -60,52 +59,52 @@
/>
<view class="filter-hint">
<text class="filter-hint-text">渠道/终端/会员/新老:待接入数据后开放</text>
<text class="filter-hint-text">娓犻亾/缁堢/浼氬憳/鏂拌€侊細寰呮帴鍏ユ暟鎹悗寮€鏀?/text>
</view>
</view>
<!-- 核心 KPI(电商化) -->
<!-- 鏍稿績 KPI锛堢數鍟嗗寲锛?-->
<view class="kpi-grid kpi-grid-6">
<view class="kpi-card">
<text class="kpi-label">新增用户</text>
<text class="kpi-label">鏂板鐢ㄦ埛</text>
<text class="kpi-value">{{ formatInt(userData.new_users) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(userData.new_user_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(userData.new_user_growth) }}</text>
</view>
<view class="kpi-card">
<text class="kpi-label">活跃用户DAU</text>
<text class="kpi-label">娲昏穬鐢ㄦ埛锛圖AU锛?/text>
<text class="kpi-value">{{ formatInt(userData.active_users) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(userData.active_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(userData.active_growth) }}</text>
</view>
<view class="kpi-card" v-if="hasOrderingData">
<text class="kpi-label">下单用户数</text>
<text class="kpi-label">涓嬪崟鐢ㄦ埛鏁?/text>
<text class="kpi-value">{{ formatInt(userData.ordering_users) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(userData.ordering_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(userData.ordering_growth) }}</text>
</view>
<view class="kpi-card" v-if="hasPaidData">
<text class="kpi-label">支付用户数</text>
<text class="kpi-label">鏀粯鐢ㄦ埛鏁?/text>
<text class="kpi-value">{{ formatInt(userData.paid_users) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(userData.paid_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(userData.paid_growth) }}</text>
</view>
<view class="kpi-card" v-if="hasNewConversionData">
<text class="kpi-label">新客转化率</text>
<text class="kpi-label">鏂板杞寲鐜?/text>
<text class="kpi-value">{{ formatPct(userData.new_user_conversion_rate) }}</text>
<text class="kpi-meta">新客 → 下单/支付</text>
<text class="kpi-meta">鏂板 鈫?涓嬪崟/鏀粯</text>
</view>
<view class="kpi-card">
<text class="kpi-label">复购率</text>
<text class="kpi-label">澶嶈喘鐜?/text>
<text class="kpi-value">{{ formatPct(userData.repurchase_rate) }}</text>
<text class="kpi-meta">较上期:{{ formatPct(userData.repurchase_growth) }}</text>
<text class="kpi-meta">杈冧笂鏈燂細{{ formatPct(userData.repurchase_growth) }}</text>
</view>
</view>
<!-- 增长与活跃趋势 -->
<!-- 澧為暱涓庢椿璺冭秼鍔?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">增长与活跃趋势</text>
<text class="card-desc">{{ selectedPeriodText }} · 新增 vs 活跃DAU</text>
<text class="card-title">澧為暱涓庢椿璺冭秼鍔?/text>
<text class="card-desc">{{ selectedPeriodText }} 路 鏂板 vs 娲昏穬锛圖AU锛?/text>
</view>
<view v-if="loading || !growthChartOption || !growthChartOption.series || growthChartOption.series.length === 0" class="chart-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
<text>{{ loading ? '鍔犺浇涓?..' : '鏆傛棤鏁版嵁' }}</text>
</view>
<EChartsView v-else class="chart-box" :option="growthChartOption" />
</view>
@@ -113,30 +112,30 @@
<view class="two-col">
<view class="card two-col-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 class="chart-loading chart-loading-sm">
<text>暂无数据 / 待接入</text>
<text>鏆傛棤鏁版嵁 / 寰呮帴鍏?/text>
</view>
</view>
<view class="card two-col-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 class="chart-loading chart-loading-sm">
<text>暂无数据 / 待接入</text>
<text>鏆傛棤鏁版嵁 / 寰呮帴鍏?/text>
</view>
</view>
</view>
<!-- 转化漏斗 -->
<!-- 杞寲婕忔枟 -->
<view class="card card-full">
<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 class="funnel">
<view class="funnel-steps">
@@ -147,49 +146,49 @@
</view>
<view class="funnel-step-metrics">
<text class="funnel-step-value">{{ formatInt(s.value) }}</text>
<text class="funnel-step-rate" v-if="idx > 0">转化:{{ formatPct(calcFunnelRate(idx)) }}</text>
<text class="funnel-step-rate" v-else>—</text>
<text class="funnel-step-rate" v-if="idx > 0">杞寲锛歿{ formatPct(calcFunnelRate(idx)) }}</text>
<text class="funnel-step-rate" v-else>鈥?/text>
</view>
</view>
</view>
<view class="funnel-empty" v-if="!hasFunnelData">
<text class="funnel-empty-text">暂无漏斗数据 / 待接入UV、PDP、加购、下单、支付</text>
<text class="funnel-empty-text">鏆傛棤婕忔枟鏁版嵁 / 寰呮帴鍏ワ細UV銆丳DP銆佸姞璐€佷笅鍗曘€佹敮浠?/text>
</view>
</view>
</view>
<!-- 留存 / 回访 -->
<!-- 鐣欏瓨 / 鍥炶 -->
<view class="card card-full">
<view class="card-head">
<text class="card-title">留存与回访</text>
<text class="card-desc">{{ selectedPeriodText }} · 1/3/7/14/30日留存(Cohort 后续补)</text>
<text class="card-title">鐣欏瓨涓庡洖璁?/text>
<text class="card-desc">{{ selectedPeriodText }} 1/3/7/14/30鏃ョ暀瀛橈紙Cohort 鍚庣画琛ワ級</text>
</view>
<view class="two-col">
<view class="two-col-item">
<view class="sub-head">
<text class="sub-title">留存曲线</text>
<text class="sub-desc">留存率趋势(待接入)</text>
<text class="sub-title">鐣欏瓨鏇茬嚎</text>
<text class="sub-desc">鐣欏瓨鐜囪秼鍔匡紙寰呮帴鍏ワ級</text>
</view>
<EChartsView class="chart-box chart-box-sm" :option="retentionChartOption" />
</view>
<view class="two-col-item">
<view class="sub-head">
<text class="sub-title">流失用户占比</text>
<text class="sub-desc">7/14天未活跃(待接入)</text>
<text class="sub-title">娴佸け鐢ㄦ埛鍗犳瘮</text>
<text class="sub-desc">7/14澶╂湭娲昏穬锛堝緟鎺ュ叆锛?/text>
</view>
<view class="metric-empty">
<text class="metric-empty-text">暂无数据 / 待接入</text>
<text class="metric-empty-text">鏆傛棤鏁版嵁 / 寰呮帴鍏?/text>
</view>
</view>
</view>
</view>
<!-- 用户分群(运营可用) -->
<!-- 鐢ㄦ埛鍒嗙兢锛堣繍钀ュ彲鐢級 -->
<view class="card card-full">
<view class="card-head">
<text class="card-title">用户分群(运营可用)</text>
<text class="card-desc">RFM / LTV / 新客分层(后续补) · 当前为基础结构占比</text>
<text class="card-title">鐢ㄦ埛鍒嗙兢锛堣繍钀ュ彲鐢級</text>
<text class="card-desc">RFM / LTV / 鏂板鍒嗗眰锛堝悗缁ˉ锛?路 褰撳墠涓哄熀纭€缁撴瀯鍗犳瘮</text>
</view>
<view class="two-col">
<view class="two-col-item">
@@ -197,24 +196,24 @@
</view>
<view class="two-col-item">
<view class="sub-head">
<text class="sub-title">用户画像(基础)</text>
<text class="sub-desc">性别/年龄/地域(待接入)</text>
<text class="sub-title">鐢ㄦ埛鐢诲儚锛堝熀纭€锛?/text>
<text class="sub-desc">鎬у埆/骞撮緞/鍦板煙锛堝緟鎺ュ叆锛?/text>
</view>
<EChartsView class="chart-box chart-box-sm" :option="profileChartOption" />
</view>
</view>
</view>
<!-- 渠道来源Acquisition -->
<!-- 娓犻亾鏉ユ簮锛圓cquisition锛?-->
<view class="card card-full">
<view class="card-head">
<text class="card-title">渠道来源</text>
<text class="card-desc">{{ selectedPeriodText }} · 渠道占比(后续可扩展渠道质量表)</text>
<text class="card-title">娓犻亾鏉ユ簮</text>
<text class="card-desc">{{ selectedPeriodText }} 路 娓犻亾鍗犳瘮锛堝悗缁彲鎵╁睍娓犻亾璐ㄩ噺琛級</text>
</view>
<EChartsView class="chart-box" :option="trafficChartOption" />
</view>
<!-- 留白 -->
<!-- 鐣欑櫧 -->
<view style="height: 24px;"></view>
</view>
</view>
@@ -248,10 +247,10 @@ const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/user-analysis')
const timePeriods = ref<Array<TimePeriod>>([
{ 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 userData = reactive<UserData>({
@@ -279,18 +278,18 @@ const segmentChartOption = ref<any>({})
const trafficChartOption = ref<any>({})
const funnelSteps = reactive<Array<FunnelStep>>([
{ step: '访问用户UV', value: 0 },
{ step: '商品详情页(PDP UV', value: 0 },
{ step: '加购用户', value: 0 },
{ step: '下单用户', value: 0 },
{ step: '支付用户', value: 0 }
{ step: '璁块棶鐢ㄦ埛锛圲V锛?, value: 0 },
{ step: '鍟嗗搧璇︽儏椤碉紙PDP UV锛?, value: 0 },
{ step: '鍔犺喘鐢ㄦ埛', value: 0 },
{ step: '涓嬪崟鐢ㄦ埛', value: 0 },
{ step: '鏀粯鐢ㄦ埛', value: 0 }
])
const loading = ref(false)
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
return p ? p.label : '7澶?
})
const hasOrderingData = computed((): boolean => {
@@ -388,12 +387,12 @@ async function loadUserData() {
growthChartOption.value = {
grid: { left: 40, right: 18, top: 20, bottom: 40 },
tooltip: { trigger: 'axis' },
legend: { data: ['新增用户', '活跃用户DAU'], bottom: 0 },
legend: { data: ['鏂板鐢ㄦ埛', '娲昏穬鐢ㄦ埛锛圖AU锛?], bottom: 0 },
xAxis: { type: 'category', data: x, axisLabel: { color: 'rgba(0,0,0,0.55)' } },
yAxis: { type: 'value', axisLabel: { color: 'rgba(0,0,0,0.55)' }, splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } } },
series: [
{ name: '新增用户', type: 'line', data: newArr, smooth: true, symbolSize: 6, areaStyle: { opacity: 0.08 } },
{ name: '活跃用户DAU', type: 'line', data: activeArr, smooth: true, symbolSize: 6 }
{ name: '鏂板鐢ㄦ埛', type: 'line', data: newArr, smooth: true, symbolSize: 6, areaStyle: { opacity: 0.08 } },
{ name: '娲昏穬鐢ㄦ埛锛圖AU锛?, type: 'line', data: activeArr, smooth: true, symbolSize: 6 }
]
}
@@ -411,7 +410,7 @@ async function loadUserData() {
legend: { top: 10, left: 'center', padding: [12, 0, 24, 0] },
series: [
{
name: '用户分群',
name: '鐢ㄦ埛鍒嗙兢',
type: 'pie',
center: ['48%', '60%'],
radius: ['42%', '66%'],
@@ -443,20 +442,20 @@ async function loadUserData() {
}
funnelSteps.splice(0, funnelSteps.length,
{ step: '访问用户UV', value: 0 },
{ step: '商品详情页(PDP UV', value: 0 },
{ step: '加购用户', value: 0 },
{ step: '下单用户', value: 0 },
{ step: '支付用户', value: 0 }
{ step: '璁块棶鐢ㄦ埛锛圲V锛?, value: 0 },
{ step: '鍟嗗搧璇︽儏椤碉紙PDP UV锛?, value: 0 },
{ step: '鍔犺喘鐢ㄦ埛', value: 0 },
{ step: '涓嬪崟鐢ㄦ埛', value: 0 },
{ step: '鏀粯鐢ㄦ埛', value: 0 }
)
retentionChartOption.value = { title: { text: '留存率(待接入)', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
activityChartOption.value = { title: { text: '活跃度(待接入)', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
comparisonChartOption.value = { title: { text: '新老用户对比(待接入)', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
profileChartOption.value = { title: { text: '用户画像(待接入:需要性别/年龄/地域字段)', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
retentionChartOption.value = { title: { text: '鐣欏瓨鐜囷紙寰呮帴鍏ワ級', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
activityChartOption.value = { title: { text: '娲昏穬搴︼紙寰呮帴鍏ワ級', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
comparisonChartOption.value = { title: { text: '鏂拌€佺敤鎴峰姣旓紙寰呮帴鍏ワ級', left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
profileChartOption.value = { title: { text: '鐢ㄦ埛鐢诲儚锛堝緟鎺ュ叆锛氶渶瑕佹€у埆/骞撮緞/鍦板煙瀛楁锛?, left: 'center', top: 10, textStyle: { fontSize: 12, color: 'rgba(0,0,0,0.55)' } }, series: [] }
} catch (e) {
console.error('loadUserData failed', e)
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '数据加载失败' }), icon: 'none', duration: 2000 })
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '鏁版嵁鍔犺浇澶辫触' }), icon: 'none', duration: 2000 })
} finally {
loading.value = false
updateTime()
@@ -491,19 +490,19 @@ function onDateRangeClear() {
function refreshData() {
loadUserData()
uni.showToast({ title: '已刷新', icon: 'success' })
uni.showToast({ title: '宸插埛鏂?, icon: 'success' })
}
function exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
itemList: ['瀵煎嚭Excel', '瀵煎嚭PDF', '瀵煎嚭鍥剧墖'],
success: () => uni.showToast({ title: '瀵煎嚭鎴愬姛', icon: 'success' })
})
}
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()
}
@@ -538,27 +537,27 @@ 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.showToast({ title: '下拉菜单', icon: 'none' })
uni.showToast({ title: '涓嬫媺鑿滃崟', icon: 'none' })
}
function handleSettings() {
uni.showToast({ title: '设置', icon: 'none' })
uni.showToast({ title: '璁剧疆', icon: 'none' })
}
</script>
@@ -569,7 +568,7 @@ function handleSettings() {
background: #f6f7fb;
}
/* 页面布局:宽屏时侧边栏+内容,窄屏时全屏内容 */
/* 椤甸潰甯冨眬锛氬灞忔椂渚ц竟鏍?鍐呭锛岀獎灞忔椂鍏ㄥ睆鍐呭 */
.page-layout {
display: flex;
flex-direction: row !important;
@@ -581,10 +580,10 @@ function handleSettings() {
min-width: 0;
display: flex;
flex-direction: column;
padding-top: 64px; /* 为固定顶部导航栏留出空间 */
padding-top: 64px; /* 涓哄浐瀹氶《閮ㄥ鑸爮鐣欏嚭绌洪棿 */
}
/* 响应式:窄屏时全屏显示 */
/* 鍝嶅簲寮忥細绐勫睆鏃跺叏灞忔樉绀?*/
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
@@ -599,11 +598,11 @@ function handleSettings() {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 16px 16px 28px;
/* padding removed */ 16px 28px;
box-sizing: border-box;
}
/* 全局筛选区 */
/* 鍏ㄥ眬绛涢€夊尯 */
.filters {
margin-top: 12px;
background: #fff;
@@ -640,7 +639,7 @@ function handleSettings() {
color: #6c757d;
}
/* 时间维度 tabs */
/* 鏃堕棿缁村害 tabs */
.tabs {
display: flex;
flex-direction: row !important;
@@ -671,7 +670,7 @@ function handleSettings() {
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
/* KPI 网格 */
/* KPI 缃戞牸 */
.kpi-grid {
margin-top: 12px;
display: grid;
@@ -741,7 +740,7 @@ function handleSettings() {
color: rgba(0,0,0,0.55);
}
/* 卡片 */
/* 鍗$墖 */
.card {
margin-top: 12px;
background: #fff;
@@ -856,7 +855,7 @@ function handleSettings() {
}
}
/* 漏斗 */
/* 婕忔枟 */
.funnel {
width: 100%;
}
@@ -950,3 +949,4 @@ function handleSettings() {
}
}
</style>