优化细节
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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; /* 建议用 px,H5 更稳 */
|
||||
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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">销量 > 100</text>
|
||||
<text class="kpi-meta">閿€閲?> 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user