数据分析ui补充完善,接入数据库
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user