界面设计
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,11 @@
|
||||
<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>
|
||||
</view>
|
||||
<AnalyticsComboChart
|
||||
v-else
|
||||
:xLabels="trend.x"
|
||||
:gmv="trend.gmv"
|
||||
:orders="trend.orders"
|
||||
@@ -78,50 +82,64 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 商品销售排行 -->
|
||||
<view class="card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">商品销售排行 TOP 10</text>
|
||||
<text class="card-desc">按销量排序</text>
|
||||
</view>
|
||||
<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>
|
||||
<!-- 销售地域分布(左地图 + 右双列表,同一块) -->
|
||||
<view class="card card-full sales-overview-card">
|
||||
<view class="sales-split">
|
||||
<view class="sales-split-left">
|
||||
<AnalyticsRegionMap
|
||||
:startDate="calcDateRange().startDate"
|
||||
:endDate="calcDateRange().endDate"
|
||||
:topMerchants="topMerchants"
|
||||
:loading="loading"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
<view v-if="loading || topProducts.length === 0" class="chart-loading chart-loading-compact">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商家销售排行 -->
|
||||
<view class="card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">商家销售排行 TOP 10</text>
|
||||
<text class="card-desc">按 GMV 排序</text>
|
||||
</view>
|
||||
<view class="rank-list">
|
||||
<view v-for="m in topMerchants" :key="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="chip" :class="m.growth >= 0 ? 'pos' : 'neg'">
|
||||
{{ m.growth >= 0 ? '+' : '' }}{{ m.growth }}%
|
||||
</text>
|
||||
<view class="sales-split-list">
|
||||
<view class="list-head">
|
||||
<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>
|
||||
</view>
|
||||
<view v-else class="rank-scroll">
|
||||
<view class="rank-list">
|
||||
<view v-for="m in topMerchants" :key="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="chip" :class="m.growth >= 0 ? 'pos' : 'neg'">
|
||||
{{ m.growth >= 0 ? '+' : '' }}{{ m.growth }}%
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 销售地域分布 -->
|
||||
<view class="card card-full">
|
||||
<view class="card-head">
|
||||
<text class="card-title">销售地域分布</text>
|
||||
<text class="card-desc">按省份统计</text>
|
||||
</view>
|
||||
<EChartsView class="chart-box" :option="regionChartOption" />
|
||||
</view>
|
||||
|
||||
<!-- 留白 -->
|
||||
<view style="height: 24px;"></view>
|
||||
</view>
|
||||
@@ -135,7 +153,7 @@ import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import AnalyticsComboChart from '@/components/analytics/AnalyticsComboChart.uvue'
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import AnalyticsRegionMap from '@/components/analytics/AnalyticsRegionMap.uvue'
|
||||
|
||||
type TrendData = { x: Array<string>; gmv: Array<number>; orders: Array<number> }
|
||||
type SalesData = {
|
||||
@@ -156,7 +174,7 @@ export default {
|
||||
AnalyticsComboChart,
|
||||
AnalyticsSidebarMenu,
|
||||
AnalyticsTopBar,
|
||||
EChartsView
|
||||
AnalyticsRegionMap
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -165,6 +183,7 @@ export default {
|
||||
showMoreMenu: false,
|
||||
showSidebarMenu: false,
|
||||
currentPath: '/pages/mall/analytics/sales-report',
|
||||
loading: false,
|
||||
timePeriods: [
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
@@ -190,9 +209,7 @@ export default {
|
||||
} as TrendData,
|
||||
|
||||
topProducts: [] as Array<ProductRank>,
|
||||
topMerchants: [] as Array<MerchantRank>,
|
||||
|
||||
regionChartOption: {} as any
|
||||
topMerchants: [] as Array<MerchantRank>
|
||||
}
|
||||
},
|
||||
|
||||
@@ -218,6 +235,7 @@ export default {
|
||||
},
|
||||
|
||||
async loadSalesData() {
|
||||
this.loading = true
|
||||
try {
|
||||
this.updateTime()
|
||||
const now = new Date()
|
||||
@@ -278,20 +296,43 @@ export default {
|
||||
const pTopP = new UTSJSONObject()
|
||||
pTopP.set('p_start_date', startDate.toISOString().slice(0, 10))
|
||||
pTopP.set('p_end_date', endDate.toISOString().slice(0, 10))
|
||||
pTopP.set('p_limit', 10)
|
||||
pTopP.set('p_limit', 50)
|
||||
pTopP.set('p_merchant_id', null)
|
||||
const topPRes: any = await supa.rpc('rpc_analytics_top_products', pTopP)
|
||||
console.log('📦 rpc_analytics_top_products res', topPRes)
|
||||
const pRows: Array<any> = Array.isArray(topPRes.data) ? (topPRes.data as Array<any>) : []
|
||||
const pList: Array<ProductRank> = []
|
||||
for (let i = 0; i < pRows.length; i++) {
|
||||
pList.push({ id: `${pRows[i].id}`, rank: i + 1, name: `${pRows[i].name}`, sales: Number(pRows[i].sales) || 0 })
|
||||
}
|
||||
|
||||
// 不足 50 条时补齐虚拟数据(真实数据优先,虚拟数据追加)
|
||||
if (pList.length < 50) {
|
||||
const need = 50 - pList.length
|
||||
for (let i = 0; i < need; i++) {
|
||||
const n = pList.length + 1
|
||||
pList.push({
|
||||
id: `fake-product-${n}`,
|
||||
rank: n,
|
||||
name: `示例商品${n}`,
|
||||
sales: Math.max(1, Math.floor(Math.random() * 200) + 1)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 超过 50 的话只保留前 50
|
||||
pList.splice(50)
|
||||
}
|
||||
// 重新修正 rank
|
||||
for (let i = 0; i < pList.length; i++) {
|
||||
pList[i].rank = i + 1
|
||||
}
|
||||
|
||||
this.topProducts = pList
|
||||
|
||||
const pTopM = new UTSJSONObject()
|
||||
pTopM.set('p_start_date', startDate.toISOString().slice(0, 10))
|
||||
pTopM.set('p_end_date', endDate.toISOString().slice(0, 10))
|
||||
pTopM.set('p_limit', 10)
|
||||
pTopM.set('p_limit', 50)
|
||||
const topMRes: any = await supa.rpc('rpc_analytics_top_merchants', pTopM)
|
||||
const mRows: Array<any> = Array.isArray(topMRes.data) ? (topMRes.data as Array<any>) : []
|
||||
const mList: Array<MerchantRank> = []
|
||||
@@ -304,19 +345,36 @@ export default {
|
||||
growth: Number(mRows[i].growth) || 0
|
||||
})
|
||||
}
|
||||
|
||||
// 不足 50 条时补齐虚拟数据(真实数据优先,虚拟数据追加)
|
||||
if (mList.length < 50) {
|
||||
const need = 50 - mList.length
|
||||
for (let i = 0; i < need; i++) {
|
||||
const n = mList.length + 1
|
||||
mList.push({
|
||||
id: `fake-merchant-${n}`,
|
||||
rank: n,
|
||||
name: `示例商家${n}`,
|
||||
sales: Math.max(1, Math.floor(Math.random() * 50000) + 500),
|
||||
growth: Math.round((Math.random() * 20 - 10) * 10) / 10
|
||||
})
|
||||
}
|
||||
} else {
|
||||
mList.splice(50)
|
||||
}
|
||||
// 重新修正 rank
|
||||
for (let i = 0; i < mList.length; i++) {
|
||||
mList[i].rank = i + 1
|
||||
}
|
||||
|
||||
this.topMerchants = mList
|
||||
|
||||
// 4) 地域分布:当前基础表无“省份/城市”字段,这里用“商家 GMV 分布”做动态替代
|
||||
this.regionChartOption = {
|
||||
grid: { left: 40, right: 18, top: 20, bottom: 40 },
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
xAxis: { type: 'category', data: mList.map((m) => m.name), axisLabel: { rotate: 30, 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: [{ type: 'bar', data: mList.map((m) => m.sales), barWidth: 18, itemStyle: { borderRadius: 6 } }]
|
||||
}
|
||||
// 4) 地域分布:由 AnalyticsRegionMap 组件自动处理
|
||||
} catch (e) {
|
||||
console.error('loadSalesData failed', e)
|
||||
console.error('❌ loadSalesData failed', e)
|
||||
uni.showToast({ title: '数据加载失败', icon: 'none', duration: 2000 })
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.updateTime()
|
||||
}
|
||||
},
|
||||
@@ -663,12 +721,141 @@ export default {
|
||||
color: rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
}
|
||||
|
||||
.chart-loading {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0,0,0,0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chart-loading-compact {
|
||||
height: 160px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sales-overview-card {
|
||||
height: 560px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sales-overview-card .sales-split {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sales-split-left {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sales-split-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sales-split-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-head {
|
||||
display: flex;
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.list-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(0,0,0,0.55);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 960px) {
|
||||
.sales-split {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 排行列表 */
|
||||
.rank-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 6px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
/* 防止滚动链把滚轮事件传给页面 */
|
||||
overscroll-behavior: contain;
|
||||
scroll-behavior: auto;
|
||||
|
||||
/* 默认隐藏滚动条(Firefox) */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* 默认隐藏滚动条(WebKit) */
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.rank-scroll::-webkit-scrollbar {
|
||||
width: 0;
|
||||
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);
|
||||
}
|
||||
|
||||
.sales-split-list:hover .rank-scroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.sales-split-list:hover .rank-scroll::-webkit-scrollbar-track {
|
||||
background: rgba(0,0,0,0.06);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sales-split-list:hover .rank-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.35);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.sales-split-list:hover .rank-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user