优化细节

This commit is contained in:
2026-02-06 12:06:33 +08:00
parent b7545173c6
commit d00f0b7412
83 changed files with 3901 additions and 2354 deletions

View File

@@ -1,8 +1,8 @@
<template>
<template>
<view class="page" @click.self="closeMoreMenu">
<!-- 固定顶部导航栏 -->
<!-- 鍥哄畾椤堕儴瀵艰埅鏍?-->
<AnalyticsTopBar
:title="'销售报表'"
:title="'閿€鍞姤琛?"
:lastUpdateTime="lastUpdateTime"
:sidebarVisible="showSidebarMenu"
@menu-click="handleMenu"
@@ -16,18 +16,18 @@
/>
<view class="page-layout">
<!-- 侧边栏菜单组件 -->
<!-- 渚ц竟鏍忚彍鍗曠粍浠?-->
<AnalyticsSidebarMenu
:visible="showSidebarMenu"
:currentPath="currentPath"
@visible-change="handleSidebarUpdate"
/>
<!-- 主内容区域 -->
<!-- 涓诲唴瀹瑰尯鍩?-->
<view class="main-content">
<view class="container">
<!-- 时间维度筛选(快捷 + 自定义) -->
<!-- 鏃堕棿缁村害绛涢€夛紙蹇嵎 + 鑷畾涔夛級 -->
<view class="tabs">
<view
v-for="p in timePeriods"
@@ -43,8 +43,7 @@
:class="{ active: customRangeEnabled }"
@click="toggleCustomRange"
>
自定义
</view>
鑷畾涔? </view>
</view>
<AnalyticsDateRangePicker
@@ -55,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>