数据分析ui补充完善,接入数据库

This commit is contained in:
comlibmb
2026-01-29 17:30:39 +08:00
parent 3e89513e8b
commit b53d2376ff
13 changed files with 1161 additions and 889 deletions

View File

@@ -109,7 +109,7 @@
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
import supa, { ensureSupabaseReady } from '@/components/supadb/aksupainstance.uts'
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
@@ -176,9 +176,160 @@ export default {
methods: {
async loadCouponData() {
// TODO: 实现优惠券数据加载
this.updateTime()
this.buildChartOptions()
try {
// 1) 计算时间范围
const now = new Date()
const start = new Date(now.getTime())
if (this.selectedPeriod === '7d') start.setDate(start.getDate() - 7)
else if (this.selectedPeriod === '30d') start.setDate(start.getDate() - 30)
else if (this.selectedPeriod === '90d') start.setDate(start.getDate() - 90)
else if (this.selectedPeriod === '1y') start.setFullYear(start.getFullYear() - 1)
const startIso = start.toISOString()
const endIso = now.toISOString()
// 2) 确保 Supabase 会话就绪
await ensureSupabaseReady()
// 3) 优先调用后端 RPC推荐在 Supabase 中创建对应函数)
// - rpc_coupon_effectiveness_overview 概览 KPI
// - rpc_coupon_type_stats 按券类型统计
// - rpc_coupon_channel_stats 发放渠道统计
// - rpc_coupon_trend_daily 每日发放/使用趋势
// - rpc_coupon_conversion_effect GMV/订单转化效果
let overviewRow: UTSJSONObject | null = null
let typeList: Array<UTSJSONObject> = []
let channelList: Array<UTSJSONObject> = []
let trendList: Array<UTSJSONObject> = []
let conversionList: Array<UTSJSONObject> = []
// 3.1 概览 KPI
const overviewRes = await supa.rpc('rpc_coupon_effectiveness_overview', {
p_start: startIso,
p_end: endIso
})
if (overviewRes.status === 404) {
// RPC 未创建:保留默认 0 值,由后续 SQL 实现补上真实逻辑
console.warn('rpc_coupon_effectiveness_overview not found, using default zeros')
} else if (overviewRes.error != null) {
throw overviewRes.error
} else {
const anyData = overviewRes.data as any
if (Array.isArray(anyData) && anyData.length > 0) {
overviewRow = anyData[0] as UTSJSONObject
}
}
// 3.2 券类型统计
const typeRes = await supa.rpc('rpc_coupon_type_stats', {
p_start: startIso,
p_end: endIso
})
if (typeRes.status === 404) {
console.warn('rpc_coupon_type_stats not found, type analysis will be empty')
} else if (typeRes.error != null) {
throw typeRes.error
} else {
const typeAny = typeRes.data as any
typeList = Array.isArray(typeAny) ? typeAny as Array<UTSJSONObject> : []
}
// 3.3 渠道统计
const channelRes = await supa.rpc('rpc_coupon_channel_stats', {
p_start: startIso,
p_end: endIso
})
if (channelRes.status === 404) {
console.warn('rpc_coupon_channel_stats not found, channel analysis will be empty')
} else if (channelRes.error != null) {
throw channelRes.error
} else {
const chAny = channelRes.data as any
channelList = Array.isArray(chAny) ? chAny as Array<UTSJSONObject> : []
}
// 3.4 使用趋势
const trendRes = await supa.rpc('rpc_coupon_trend_daily', {
p_start: startIso,
p_end: endIso
})
if (trendRes.status === 404) {
console.warn('rpc_coupon_trend_daily not found, trend analysis will be empty')
} else if (trendRes.error != null) {
throw trendRes.error
} else {
const trAny = trendRes.data as any
trendList = Array.isArray(trAny) ? trAny as Array<UTSJSONObject> : []
}
// 3.5 转化效果GMV/订单)
const convRes = await supa.rpc('rpc_coupon_conversion_effect', {
p_start: startIso,
p_end: endIso
})
if (convRes.status === 404) {
console.warn('rpc_coupon_conversion_effect not found, conversion chart will be empty')
} else if (convRes.error != null) {
throw convRes.error
} else {
const cvAny = convRes.data as any
conversionList = Array.isArray(cvAny) ? cvAny as Array<UTSJSONObject> : []
}
// 4) 计算 KPI 概览
let totalIssued = 0
let totalUsed = 0
let gmvIncrease = 0.0
let issuedGrowth = 0.0
let usageRate = 0.0
let gmvGrowth = 0.0
let roi = 0.0
if (overviewRow != null) {
totalIssued = overviewRow.getNumber('total_issued') ?? 0
totalUsed = overviewRow.getNumber('total_used') ?? 0
gmvIncrease = overviewRow.getNumber('gmv_increase') ?? 0
issuedGrowth = overviewRow.getNumber('issued_growth') ?? 0
usageRate = overviewRow.getNumber('usage_rate') ?? 0
gmvGrowth = overviewRow.getNumber('gmv_growth') ?? 0
roi = overviewRow.getNumber('roi') ?? 0
} else {
// 概览 RPC 不存在时,使用类型统计简单近似(只保证页面可用)
for (let i = 0; i < typeList.length; i++) {
const r = typeList[i]
totalIssued += r.getNumber('total_issued') ?? 0
totalUsed += r.getNumber('total_used') ?? 0
}
if (totalIssued > 0) {
usageRate = (totalUsed / totalIssued) * 100
}
}
this.couponData = {
total_issued: totalIssued,
issued_growth: issuedGrowth,
total_used: totalUsed,
usage_rate: usageRate,
gmv_increase: gmvIncrease,
gmv_growth: gmvGrowth,
roi: roi
} as CouponData
// 将原始行数据挂到实例上,方便绘制图表
;(this as any)._typeRows = typeList
;(this as any)._channelRows = channelList
;(this as any)._trendRows = trendList
;(this as any)._conversionRows = conversionList
this.updateTime()
this.buildChartOptions()
} catch (e) {
console.error('loadCouponData failed:', e)
this.updateTime()
this.buildChartOptions()
uni.showToast({ title: '优惠券分析数据加载失败', icon: 'none' })
}
},
selectPeriod(p: string) {
@@ -224,11 +375,176 @@ export default {
},
buildChartOptions() {
// TODO: 构建图表配置
this.typeChartOption = {}
this.channelChartOption = {}
this.trendChartOption = {}
this.conversionChartOption = {}
const typeAny = (this as any)._typeRows as any
const channelAny = (this as any)._channelRows as any
const trendAny = (this as any)._trendRows as any
const convAny = (this as any)._conversionRows as any
const typeRows = Array.isArray(typeAny) ? typeAny as Array<UTSJSONObject> : []
const channelRows = Array.isArray(channelAny) ? channelAny as Array<UTSJSONObject> : []
const trendRows = Array.isArray(trendAny) ? trendAny as Array<UTSJSONObject> : []
const convRows = Array.isArray(convAny) ? convAny as Array<UTSJSONObject> : []
// 1) 券类型分析:柱状图(发放/使用/使用率)
const typeNames: string[] = []
const typeIssued: number[] = []
const typeUsed: number[] = []
const typeUsageRate: number[] = []
for (let i = 0; i < typeRows.length; i++) {
const r = typeRows[i]
const t = r.getNumber('coupon_type') ?? 0
// 映射 coupon_type 枚举到中文名称1..8
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)
typeUsageRate.push(r.getNumber('usage_rate') ?? 0)
}
this.typeChartOption = {
tooltip: { trigger: 'axis' },
legend: {
data: ['发放数量', '使用数量', '使用率'],
top: 'bottom'
},
// 增加 top 间距,避免左侧“数量”与上方说明文字发生遮挡
grid: { left: 40, right: 40, top: 40, bottom: 60 },
xAxis: {
type: 'category',
data: typeNames,
axisLabel: { interval: 0, rotate: 20 }
},
yAxis: [
{ type: 'value', name: '数量' },
{ type: 'value', name: '使用率', min: 0, max: 100, position: 'right' }
],
series: [
{
name: '发放数量',
type: 'bar',
data: typeIssued,
barMaxWidth: 22,
itemStyle: { color: '#3b82f6' }
},
{
name: '使用数量',
type: 'bar',
data: typeUsed,
barMaxWidth: 22,
itemStyle: { color: '#22c55e' }
},
{
name: '使用率',
type: 'line',
yAxisIndex: 1,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 2, color: '#111827' },
itemStyle: { color: '#111827' },
z: 5,
data: typeUsageRate
}
]
}
// 2) 发放渠道效果:条形图
const channelNames: string[] = []
const channelIssued: number[] = []
const channelUsed: number[] = []
for (let i = 0; i < channelRows.length; i++) {
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 = '未知'
channelNames.push(chLabel)
channelIssued.push(r.getNumber('total_issued') ?? 0)
channelUsed.push(r.getNumber('total_used') ?? 0)
}
this.channelChartOption = {
tooltip: { trigger: 'axis' },
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 }
]
}
// 3) 使用趋势:发放 vs 使用
const trendDays: string[] = []
const trendIssued: number[] = []
const trendUsed: number[] = []
for (let i = 0; i < trendRows.length; i++) {
const r = trendRows[i]
const day = r.getString('day') ?? ''
trendDays.push(day.length >= 10 ? day.substring(5, 10) : day)
trendIssued.push(r.getNumber('issued') ?? 0)
trendUsed.push(r.getNumber('used') ?? 0)
}
this.trendChartOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['发放数量', '使用数量'], top: 'bottom' },
// 增加顶部间距,避免“数量”与上方说明文字遮挡
grid: { left: 40, right: 20, top: 40, bottom: 60 },
xAxis: { type: 'category', data: trendDays },
yAxis: { type: 'value', name: '数量' },
series: [
{ name: '发放数量', type: 'bar', data: trendIssued },
{ name: '使用数量', type: 'line', smooth: true, data: trendUsed }
]
}
// 4) 转化效果:对比有券/无券 GMV & 订单数
const convNames: string[] = []
const convWith: number[] = []
const convWithout: number[] = []
for (let i = 0; i < convRows.length; i++) {
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 = '未知'
convNames.push(metricLabel)
convWith.push(r.getNumber('with_coupon') ?? 0)
convWithout.push(r.getNumber('without_coupon') ?? 0)
}
this.conversionChartOption = {
tooltip: { trigger: 'axis' },
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 }
]
}
},
toggleMoreMenu() {