数据分析ui补充完善,接入数据库
This commit is contained in:
@@ -108,370 +108,353 @@
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
<script setup lang="uts">
|
||||
import { computed, onLoad, ref } from 'vue'
|
||||
import AnalyticsSidebarMenu from '@/components/analytics/AnalyticsSidebarMenu.uvue'
|
||||
import AnalyticsTopBar from '@/components/analytics/AnalyticsTopBar.uvue'
|
||||
import EChartsView from '@/uni_modules/charts/EChartsView.vue'
|
||||
import { fetchCouponAnalysis } from '@/services/analytics/couponAnalysisService.uts'
|
||||
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
|
||||
import type { CouponData } from '@/types/analytics/coupon.uts'
|
||||
import type { TimePeriod } from '@/types/analytics/common.uts'
|
||||
|
||||
type TimePeriod = { value: string; label: string }
|
||||
type CouponData = {
|
||||
total_issued: number
|
||||
issued_growth: number
|
||||
total_used: number
|
||||
usage_rate: number
|
||||
gmv_increase: number
|
||||
gmv_growth: number
|
||||
roi: number
|
||||
}
|
||||
const lastUpdateTime = ref('')
|
||||
const selectedPeriod = ref('7d')
|
||||
const showMoreMenu = ref(false)
|
||||
const showSidebarMenu = ref(false)
|
||||
const currentPath = ref('/pages/mall/analytics/coupon-analysis')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AnalyticsSidebarMenu,
|
||||
AnalyticsTopBar,
|
||||
EChartsView
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastUpdateTime: '',
|
||||
selectedPeriod: '7d',
|
||||
showMoreMenu: false,
|
||||
showSidebarMenu: false,
|
||||
currentPath: '/pages/mall/analytics/coupon-analysis',
|
||||
timePeriods: [
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
] as Array<TimePeriod>,
|
||||
const timePeriods = ref<Array<TimePeriod>>([
|
||||
{ value: '7d', label: '7天' },
|
||||
{ value: '30d', label: '30天' },
|
||||
{ value: '90d', label: '90天' },
|
||||
{ value: '1y', label: '1年' }
|
||||
])
|
||||
|
||||
couponData: {
|
||||
total_issued: 0,
|
||||
issued_growth: 0,
|
||||
total_used: 0,
|
||||
usage_rate: 0,
|
||||
gmv_increase: 0,
|
||||
gmv_growth: 0,
|
||||
roi: 0
|
||||
} as CouponData,
|
||||
const couponData = ref<CouponData>({
|
||||
total_issued: 0,
|
||||
issued_growth: 0,
|
||||
total_used: 0,
|
||||
usage_rate: 0,
|
||||
gmv_increase: 0,
|
||||
gmv_growth: 0,
|
||||
roi: 0
|
||||
})
|
||||
|
||||
typeChartOption: {} as any,
|
||||
channelChartOption: {} as any,
|
||||
trendChartOption: {} as any,
|
||||
conversionChartOption: {} as any
|
||||
const typeChartOption = ref({} as any)
|
||||
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>>([])
|
||||
const _conversionRows = ref<Array<UTSJSONObject>>([])
|
||||
|
||||
const selectedPeriodText = computed(() => {
|
||||
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
|
||||
return p ? p.label : '7天'
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
updateTime()
|
||||
loadCouponData()
|
||||
})
|
||||
|
||||
async function loadCouponData() {
|
||||
try {
|
||||
const data = await fetchCouponAnalysis(selectedPeriod.value)
|
||||
|
||||
const overviewRow = data.overviewRow
|
||||
const typeList = data.typeList
|
||||
const channelList = data.channelList
|
||||
const trendList = data.trendList
|
||||
const conversionList = data.conversionList
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedPeriodText(): string {
|
||||
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
|
||||
return p ? p.label : '7天'
|
||||
couponData.value = {
|
||||
total_issued: totalIssued,
|
||||
issued_growth: issuedGrowth,
|
||||
total_used: totalUsed,
|
||||
usage_rate: usageRate,
|
||||
gmv_increase: gmvIncrease,
|
||||
gmv_growth: gmvGrowth,
|
||||
roi: roi
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.updateTime()
|
||||
this.loadCouponData()
|
||||
},
|
||||
_typeRows.value = typeList
|
||||
_channelRows.value = channelList
|
||||
_trendRows.value = trendList
|
||||
_conversionRows.value = conversionList
|
||||
|
||||
methods: {
|
||||
async loadCouponData() {
|
||||
try {
|
||||
const data = await fetchCouponAnalysis(this.selectedPeriod)
|
||||
|
||||
const overviewRow = data.overviewRow
|
||||
const typeList = data.typeList
|
||||
const channelList = data.channelList
|
||||
const trendList = data.trendList
|
||||
const conversionList = data.conversionList
|
||||
|
||||
// 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: mapAnalyticsError(e, { fallbackMessage: '优惠券分析数据加载失败' }), icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
selectPeriod(p: string) {
|
||||
this.selectedPeriod = p
|
||||
this.loadCouponData()
|
||||
},
|
||||
|
||||
refreshData() {
|
||||
this.loadCouponData()
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
updateTime() {
|
||||
const now = new Date()
|
||||
const hh = now.getHours().toString().padStart(2, '0')
|
||||
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||
this.lastUpdateTime = `${hh}:${mm}`
|
||||
},
|
||||
|
||||
formatInt(n: number): string {
|
||||
const v = isFinite(n) ? Math.round(n) : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toString()
|
||||
},
|
||||
|
||||
formatMoney(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万'
|
||||
return v.toFixed(0)
|
||||
},
|
||||
|
||||
formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
},
|
||||
|
||||
buildChartOptions() {
|
||||
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() {
|
||||
this.showMoreMenu = !this.showMoreMenu
|
||||
},
|
||||
|
||||
closeMoreMenu() {
|
||||
this.showMoreMenu = false
|
||||
},
|
||||
handleSidebarUpdate(visible: boolean) {
|
||||
this.showSidebarMenu = visible
|
||||
},
|
||||
|
||||
handleMenu() {
|
||||
this.showSidebarMenu = true
|
||||
}
|
||||
updateTime()
|
||||
buildChartOptions()
|
||||
} catch (e) {
|
||||
console.error('loadCouponData failed:', e)
|
||||
updateTime()
|
||||
buildChartOptions()
|
||||
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '优惠券分析数据加载失败' }), icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
function selectPeriod(p: string) {
|
||||
selectedPeriod.value = p
|
||||
loadCouponData()
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
loadCouponData()
|
||||
uni.showToast({ title: '已刷新', icon: 'success' })
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['导出Excel', '导出PDF', '导出图片'],
|
||||
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
|
||||
})
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
const now = new Date()
|
||||
const hh = now.getHours().toString().padStart(2, '0')
|
||||
const mm = now.getMinutes().toString().padStart(2, '0')
|
||||
lastUpdateTime.value = `${hh}:${mm}`
|
||||
}
|
||||
|
||||
function formatInt(n: number): string {
|
||||
const v = isFinite(n) ? Math.round(n) : 0
|
||||
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) + '万'
|
||||
return v.toFixed(0)
|
||||
}
|
||||
|
||||
function formatPct(n: number): string {
|
||||
const v = isFinite(n) ? n : 0
|
||||
const sign = v > 0 ? '+' : ''
|
||||
return `${sign}${v.toFixed(1)}%`
|
||||
}
|
||||
|
||||
function buildChartOptions() {
|
||||
const typeRows = _typeRows.value
|
||||
const channelRows = _channelRows.value
|
||||
const trendRows = _trendRows.value
|
||||
const convRows = _conversionRows.value
|
||||
|
||||
// 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
|
||||
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)
|
||||
}
|
||||
|
||||
typeChartOption.value = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: {
|
||||
data: ['发放数量', '使用数量', '使用率'],
|
||||
top: 'bottom'
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
channelChartOption.value = {
|
||||
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) 使用趋势
|
||||
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)
|
||||
}
|
||||
|
||||
trendChartOption.value = {
|
||||
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) 转化效果
|
||||
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)
|
||||
}
|
||||
|
||||
conversionChartOption.value = {
|
||||
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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMoreMenu() {
|
||||
showMoreMenu.value = !showMoreMenu.value
|
||||
}
|
||||
|
||||
function closeMoreMenu() {
|
||||
showMoreMenu.value = false
|
||||
}
|
||||
|
||||
function handleSidebarUpdate(visible: boolean) {
|
||||
showSidebarMenu.value = visible
|
||||
}
|
||||
|
||||
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' }) }
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user