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

This commit is contained in:
comlibmb
2026-01-31 21:47:42 +08:00
parent 8f181b2b6a
commit 6716398175
71 changed files with 6501 additions and 10593 deletions

View File

@@ -106,7 +106,6 @@
</view>
</view>
<!-- 留白 -->
<view style="height: 24px;"></view>
</view>
@@ -115,338 +114,326 @@
</view>
</template>
<script lang="uts">
<script setup lang="uts">
import { computed, onLoad, reactive, 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 { fetchDeliveryAnalysis } from '@/services/analytics/deliveryAnalysisService.uts'
import { mapAnalyticsError } from '@/services/analytics/errorMapper.uts'
type DeliveryData = {
avg_delivery_time: number
time_growth: number
total_fee: number
avg_fee: number
avg_orders_per_driver: number
satisfaction_rate: number
satisfaction_growth: number
}
type DriverRank = { id: string; rank: number; name: string; orders: number; rating: number }
import type { TimePeriod } from '@/types/analytics/common.uts'
import type { DeliveryData, DriverRank } from '@/types/analytics/delivery.uts'
export default {
components: {
AnalyticsSidebarMenu,
AnalyticsTopBar,
EChartsView
},
data() {
return {
lastUpdateTime: '',
selectedPeriod: '7d',
showMoreMenu: false,
showSidebarMenu: false,
currentPath: '/pages/mall/analytics/delivery-analysis',
timePeriods: [
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
{ value: '90d', label: '90天' },
{ value: '1y', label: '1年' }
],
const lastUpdateTime = ref('')
const selectedPeriod = ref('7d')
const showMoreMenu = ref(false)
const showSidebarMenu = ref(false)
const currentPath = ref('/pages/mall/analytics/delivery-analysis')
deliveryData: {
avg_delivery_time: 0,
time_growth: 0,
total_fee: 0,
avg_fee: 0,
avg_orders_per_driver: 0,
satisfaction_rate: 0,
satisfaction_growth: 0
} as DeliveryData,
const timePeriods = ref<Array<TimePeriod>>([
{ value: '7d', label: '7天' },
{ value: '30d', label: '30天' },
{ value: '90d', label: '90天' },
{ value: '1y', label: '1年' }
])
topDrivers: [] as Array<DriverRank>,
const deliveryData = reactive<DeliveryData>({
avg_delivery_time: 0,
time_growth: 0,
total_fee: 0,
avg_fee: 0,
avg_orders_per_driver: 0,
satisfaction_rate: 0,
satisfaction_growth: 0
})
timeChartOption: {} as any,
feeChartOption: {} as any,
isRankHover: false
const topDrivers = reactive<Array<DriverRank>>([])
const timeChartOption = ref<any>({})
const feeChartOption = ref<any>({})
const isRankHover = ref(false)
const _trendRows = ref<Array<UTSJSONObject>>([])
const selectedPeriodText = computed((): string => {
const p = timePeriods.value.find((t) => t.value === selectedPeriod.value)
return p ? p.label : '7天'
})
onLoad(() => {
updateTime()
loadDeliveryData()
})
async function loadDeliveryData() {
try {
const data: any = await fetchDeliveryAnalysis(selectedPeriod.value)
const trendList = data.trendList
const topList = data.topList
const trendRows: Array<UTSJSONObject> = []
let totalFee = 0
let totalOrders = 0
for (let i = 0; i < trendList.length; i++) {
const r = trendList[i]
const dayStr = r.getString('day') ?? ''
const orders = r.getNumber('completed_orders') ?? 0
const avgMin = r.getNumber('avg_delivery_minutes') ?? 0
const avgFee = r.getNumber('avg_fee') ?? 0
const tFee = r.getNumber('total_fee') ?? 0
totalOrders += orders
totalFee += tFee
const obj = new UTSJSONObject()
obj.set('day', dayStr)
obj.set('avg_delivery_time', avgMin)
obj.set('avg_fee', avgFee)
obj.set('satisfaction_rate', 0)
trendRows.push(obj)
}
},
computed: {
selectedPeriodText(): string {
const p = this.timePeriods.find((t) => t.value === this.selectedPeriod)
return p ? p.label : '7天'
}
},
onLoad() {
this.updateTime()
this.loadDeliveryData()
},
methods: {
async loadDeliveryData() {
try {
const data = await fetchDeliveryAnalysis(this.selectedPeriod)
const trendList = data.trendList
const topList = data.topList
// 3) 转成页面内部 trendRows 格式
const trendRows: Array<UTSJSONObject> = []
let totalFee = 0
let totalOrders = 0
for (let i = 0; i < trendList.length; i++) {
const r = trendList[i]
const dayStr = r.getString('day') ?? ''
const orders = r.getNumber('completed_orders') ?? 0
const avgMin = r.getNumber('avg_delivery_minutes') ?? 0
const avgFee = r.getNumber('avg_fee') ?? 0
const tFee = r.getNumber('total_fee') ?? 0
totalOrders += orders
totalFee += tFee
const obj = new UTSJSONObject()
obj.set('day', dayStr)
obj.set('avg_delivery_time', avgMin)
obj.set('avg_fee', avgFee)
// 满意度趋势:目前来源为配送员表 rating_avg后续如有配送评价表可替换
obj.set('satisfaction_rate', 0)
trendRows.push(obj)
}
// 4) 满意度:用 TOP10 里的 rating_avg 做平均(简单可用;也可以后续改为全量司机或配送评价表)
let satisSum = 0
let satisCnt = 0
for (let i = 0; i < topList.length; i++) {
const r = topList[i]
const rating = r.getNumber('rating_avg')
if (rating != null) {
satisSum += rating
satisCnt += 1
}
}
const satisAvg = satisCnt > 0 ? (satisSum / satisCnt) : 0
for (let i = 0; i < trendRows.length; i++) {
trendRows[i].set('satisfaction_rate', satisAvg)
}
// 5) KPI最后一天 vs 前一天环比
const last = trendRows.length > 0 ? trendRows[trendRows.length - 1] : null
const prev = trendRows.length > 1 ? trendRows[trendRows.length - 2] : null
const lastAvgTime = last != null ? (last.getNumber('avg_delivery_time') ?? 0) : 0
const prevAvgTime = prev != null ? (prev.getNumber('avg_delivery_time') ?? 0) : 0
const timeGrowth = prevAvgTime > 0 ? ((lastAvgTime - prevAvgTime) / prevAvgTime) * 100 : 0
const lastSatis = last != null ? (last.getNumber('satisfaction_rate') ?? 0) : 0
const prevSatis = prev != null ? (prev.getNumber('satisfaction_rate') ?? 0) : 0
const satisGrowth = prevSatis > 0 ? ((lastSatis - prevSatis) / prevSatis) * 100 : 0
// 配送员效率:单/人/天(按 TOP10 近似人数 + 趋势天数)
const dayCount = Math.max(1, trendRows.length)
const driverCount = Math.max(1, topList.length)
const avgOrdersPerDriverPerDay = (totalOrders / driverCount) / dayCount
this.deliveryData = {
avg_delivery_time: Math.round(lastAvgTime),
time_growth: timeGrowth,
total_fee: totalFee,
avg_fee: totalOrders > 0 ? (totalFee / totalOrders) : 0,
avg_orders_per_driver: avgOrdersPerDriverPerDay,
satisfaction_rate: lastSatis,
satisfaction_growth: satisGrowth
} as DeliveryData
;(this as any)._trendRows = trendRows
// 6) TOP10 映射
const list: DriverRank[] = []
for (let i = 0; i < topList.length; i++) {
const r = topList[i]
list.push({
id: r.getString('driver_id') ?? String(i),
rank: i + 1,
name: r.getString('driver_name') ?? '未知',
orders: r.getNumber('orders') ?? 0,
rating: r.getNumber('rating_avg') ?? 0
} as DriverRank)
}
this.topDrivers = list
this.updateTime()
this.buildChartOptions()
} catch (e) {
console.error('loadDeliveryData failed:', e)
this.updateTime()
this.buildChartOptions()
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '配送分析数据加载失败' }), icon: 'none' })
let satisSum = 0
let satisCnt = 0
for (let i = 0; i < topList.length; i++) {
const r = topList[i]
const rating = r.getNumber('rating_avg')
if (rating != null) {
satisSum += rating
satisCnt += 1
}
},
}
const satisAvg = satisCnt > 0 ? satisSum / satisCnt : 0
for (let i = 0; i < trendRows.length; i++) {
trendRows[i].set('satisfaction_rate', satisAvg)
}
selectPeriod(p: string) {
this.selectedPeriod = p
this.loadDeliveryData()
},
const last = trendRows.length > 0 ? trendRows[trendRows.length - 1] : null
const prev = trendRows.length > 1 ? trendRows[trendRows.length - 2] : null
refreshData() {
this.loadDeliveryData()
uni.showToast({ title: '已刷新', icon: 'success' })
},
const lastAvgTime = last != null ? last.getNumber('avg_delivery_time') ?? 0 : 0
const prevAvgTime = prev != null ? prev.getNumber('avg_delivery_time') ?? 0 : 0
const timeGrowth = prevAvgTime > 0 ? ((lastAvgTime - prevAvgTime) / prevAvgTime) * 100 : 0
exportReport() {
uni.showActionSheet({
itemList: ['导出Excel', '导出PDF', '导出图片'],
success: () => uni.showToast({ title: '导出成功', icon: 'success' })
const lastSatis = last != null ? last.getNumber('satisfaction_rate') ?? 0 : 0
const prevSatis = prev != null ? prev.getNumber('satisfaction_rate') ?? 0 : 0
const satisGrowth = prevSatis > 0 ? ((lastSatis - prevSatis) / prevSatis) * 100 : 0
const dayCount = Math.max(1, trendRows.length)
const driverCount = Math.max(1, topList.length)
const avgOrdersPerDriverPerDay = (totalOrders / driverCount) / dayCount
deliveryData.avg_delivery_time = Math.round(lastAvgTime)
deliveryData.time_growth = timeGrowth
deliveryData.total_fee = totalFee
deliveryData.avg_fee = totalOrders > 0 ? totalFee / totalOrders : 0
deliveryData.avg_orders_per_driver = avgOrdersPerDriverPerDay
deliveryData.satisfaction_rate = lastSatis
deliveryData.satisfaction_growth = satisGrowth
_trendRows.value = trendRows
const list: Array<DriverRank> = []
for (let i = 0; i < topList.length; i++) {
const r = topList[i]
list.push({
id: r.getString('driver_id') ?? String(i),
rank: i + 1,
name: r.getString('driver_name') ?? '未知',
orders: r.getNumber('orders') ?? 0,
rating: r.getNumber('rating_avg') ?? 0
})
},
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(2)
},
formatPct(n: number): string {
const v = isFinite(n) ? n : 0
const sign = v > 0 ? '+' : ''
return `${sign}${v.toFixed(1)}%`
},
formatScore(n: number): string {
const v = isFinite(n) ? n : 0
return v.toFixed(1)
},
buildChartOptions() {
const rowsAny = (this as any)._trendRows as any
const rows = Array.isArray(rowsAny) ? rowsAny as Array<UTSJSONObject> : []
const xAxis: string[] = []
const timeSeries: number[] = []
const feeSeries: number[] = []
const satisSeries: number[] = []
for (let i = 0; i < rows.length; i++) {
const r = rows[i]
const day = r.getString('day') ?? ''
xAxis.push(day.length >= 10 ? day.substring(5, 10) : day)
timeSeries.push(r.getNumber('avg_delivery_time') ?? 0)
feeSeries.push(r.getNumber('avg_fee') ?? 0)
satisSeries.push(r.getNumber('satisfaction_rate') ?? 0)
}
this.timeChartOption = {
tooltip: { trigger: 'axis' },
legend: {
data: ['平均配送时间(分钟)', '满意度(评分)'],
top: 'bottom',
itemGap: 30,
itemWidth: 16,
itemHeight: 16
},
grid: { left: 40, right: 50, top: 30, bottom: 60 },
xAxis: {
type: 'category',
data: xAxis,
axisTick: { alignWithLabel: true }
},
yAxis: [
{
type: 'value',
name: '配送时间',
min: 0,
splitLine: { lineStyle: { color: '#e5e7eb' } }
},
{
type: 'value',
name: '满意度',
min: 0,
max: 5,
position: 'right',
splitLine: { show: false }
}
],
series: [
{
name: '平均配送时间(分钟)',
type: 'line',
smooth: true,
symbolSize: 6,
data: timeSeries,
yAxisIndex: 0
},
{
name: '满意度(评分)',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: satisSeries,
yAxisIndex: 1
}
]
}
this.feeChartOption = {
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value' },
series: [
{
name: '平均配送费(元)',
type: 'bar',
data: feeSeries
}
]
}
this.satisfactionChartOption = {
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value', min: 0, max: 5 },
series: [
{
name: '满意度(评分)',
type: 'line',
smooth: true,
data: satisSeries
}
]
}
},
handleMenu() {
this.showSidebarMenu = true
},
handleSidebarUpdate(visible: boolean) {
this.showSidebarMenu = visible
},
toggleMoreMenu() {
this.showMoreMenu = !this.showMoreMenu
},
closeMoreMenu() {
this.showMoreMenu = false
}
topDrivers.splice(0, topDrivers.length, ...list)
updateTime()
buildChartOptions()
} catch (e) {
console.error('loadDeliveryData failed:', e)
updateTime()
buildChartOptions()
uni.showToast({ title: mapAnalyticsError(e, { fallbackMessage: '配送分析数据加载失败' }), icon: 'none' })
}
}
function selectPeriod(p: string) {
selectedPeriod.value = p
loadDeliveryData()
}
function refreshData() {
loadDeliveryData()
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(2)
}
function formatPct(n: number): string {
const v = isFinite(n) ? n : 0
const sign = v > 0 ? '+' : ''
return `${sign}${v.toFixed(1)}%`
}
function formatScore(n: number): string {
const v = isFinite(n) ? n : 0
return v.toFixed(1)
}
function buildChartOptions() {
const rows = Array.isArray(_trendRows.value) ? _trendRows.value : []
const xAxis: string[] = []
const timeSeries: number[] = []
const feeSeries: number[] = []
const satisSeries: number[] = []
for (let i = 0; i < rows.length; i++) {
const r = rows[i]
const day = r.getString('day') ?? ''
xAxis.push(day.length >= 10 ? day.substring(5, 10) : day)
timeSeries.push(r.getNumber('avg_delivery_time') ?? 0)
feeSeries.push(r.getNumber('avg_fee') ?? 0)
satisSeries.push(r.getNumber('satisfaction_rate') ?? 0)
}
timeChartOption.value = {
tooltip: { trigger: 'axis' },
legend: {
data: ['平均配送时间(分钟)', '满意度(评分)'],
top: 'bottom',
itemGap: 30,
itemWidth: 16,
itemHeight: 16
},
grid: { left: 40, right: 50, top: 30, bottom: 60 },
xAxis: {
type: 'category',
data: xAxis,
axisTick: { alignWithLabel: true }
},
yAxis: [
{
type: 'value',
name: '配送时间',
min: 0,
splitLine: { lineStyle: { color: '#e5e7eb' } }
},
{
type: 'value',
name: '满意度',
min: 0,
max: 5,
position: 'right',
splitLine: { show: false }
}
],
series: [
{
name: '平均配送时间(分钟)',
type: 'line',
smooth: true,
symbolSize: 6,
data: timeSeries,
yAxisIndex: 0
},
{
name: '满意度(评分)',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: satisSeries,
yAxisIndex: 1
}
]
}
feeChartOption.value = {
tooltip: { trigger: 'axis' },
grid: { left: 40, right: 20, top: 20, bottom: 30 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value' },
series: [
{
name: '平均配送费(元)',
type: 'bar',
data: feeSeries
}
]
}
}
function handleMenu() {
showSidebarMenu.value = true
}
function handleSidebarUpdate(visible: boolean) {
showSidebarMenu.value = visible
}
function toggleMoreMenu() {
showMoreMenu.value = !showMoreMenu.value
}
function closeMoreMenu() {
showMoreMenu.value = false
}
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' })
}
function onRankHover(hover: boolean) {
isRankHover.value = hover
}
</script>
<style>
@@ -717,9 +704,7 @@ export default {
height: 360px;
}
/* 排行滚动容器:固定高度(约 5 条) */
.rank-scroll {
/* 5条左右的可视高度5*(10px上下padding + 28px内容 + 10px gap) 约 300 */
height: 320px;
border-radius: 12px;
}
@@ -728,7 +713,6 @@ export default {
height: 100%;
}
/* H5默认隐藏滚动条悬停时显示 */
.rank-scroll-inner::-webkit-scrollbar {
width: 0;
height: 0;
@@ -747,7 +731,6 @@ export default {
background: rgba(0, 0, 0, 0.06);
}
/* 排行列表 */
.rank-list {
display: flex;
flex-direction: column;
@@ -812,7 +795,6 @@ export default {
color: #dc2626;
}
/* 响应式 */
@media screen and (min-width: 960px) {
.kpi-card {
flex: 1 1 calc(25% - 9px);
@@ -825,22 +807,21 @@ export default {
.subtitle {
max-width: 200px;
}
.topbar-right .btn-hidden {
display: none !important;
}
.more-btn {
display: flex !important;
}
}
/* 响应式:窄屏时全屏显示 */
@media screen and (max-width: 959px) {
.page-layout {
flex-direction: column !important;
}
.main-content {
width: 100%;
}